From 3ee5ccafb508eff3e44b83b7f8e239ac6f9efe63 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 1 May 2024 17:06:40 -0700 Subject: [PATCH 01/65] fix: add starting code for ccip022 proposal --- ...ccip022-citycoins-treasury-redemption.clar | 335 ++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100644 contracts/proposals/ccip022-citycoins-treasury-redemption.clar diff --git a/contracts/proposals/ccip022-citycoins-treasury-redemption.clar b/contracts/proposals/ccip022-citycoins-treasury-redemption.clar new file mode 100644 index 0000000..b547c91 --- /dev/null +++ b/contracts/proposals/ccip022-citycoins-treasury-redemption.clar @@ -0,0 +1,335 @@ +;; TRAITS + +(impl-trait .proposal-trait.proposal-trait) +(impl-trait .ccip015-trait.ccip015-trait) + +;; ERRORS + +(define-constant ERR_PANIC (err u2200)) +(define-constant ERR_VOTED_ALREADY (err u2201)) +(define-constant ERR_NOTHING_STACKED (err u2202)) +(define-constant ERR_USER_NOT_FOUND (err u2203)) +(define-constant ERR_PROPOSAL_NOT_ACTIVE (err u2204)) +(define-constant ERR_PROPOSAL_STILL_ACTIVE (err u2205)) +(define-constant ERR_VOTE_FAILED (err u2206)) + +;; CONSTANTS + +;; used to pay out missed cycles in protocol +(define-constant MISSED_PAYOUT u1) + +(define-constant SELF (as-contract tx-sender)) +(define-constant CCIP_022 { + name: "CityCoins Treasury Redemption", + link: "https://github.com/citycoins/governance/blob/feat/add-ccip-022/ccips/ccip-022/ccip-022-citycoins-treasury-redemption.md", + hash: "TBD", +}) + +(define-constant MIA_ID u1) ;; (contract-call? .ccd004-city-registry get-city-id "mia") +(define-constant NYC_ID u2) ;; (contract-call? .ccd004-city-registry get-city-id "nyc") + +(define-constant VOTE_SCALE_FACTOR (pow u10 u16)) ;; 16 decimal places +(define-constant MIA_SCALE_BASE (pow u10 u4)) ;; 4 decimal places +(define-constant MIA_SCALE_FACTOR u8916) ;; 0.8916 or 89.16% +;; MIA votes scaled to make 1 MIA = 1 NYC +;; full calculation available in CCIP-022 + +;; DATA VARS + +;; vote block heights +(define-data-var voteActive bool true) +(define-data-var voteStart uint u0) +(define-data-var voteEnd uint u0) + +;; start the vote when deployed +(var-set voteStart block-height) + +;; DATA MAPS + +(define-map CityVotes + uint ;; city ID + { ;; vote + totalAmountYes: uint, + totalAmountNo: uint, + totalVotesYes: uint, + totalVotesNo: uint, + } +) + +(define-map UserVotes + uint ;; user ID + { ;; vote + vote: bool, + mia: uint, + nyc: uint, + } +) +;; PUBLIC FUNCTIONS + +(define-public (execute (sender principal)) + (begin + ;; check vote is complete/passed + (try! (is-executable)) + ;; update vote variables + (var-set voteEnd block-height) + (var-set voteActive false) + ;; transfer funds to new redemption extensions + (try! (contract-call? .ccd002-treasury-mia-mining-v2 withdraw-stx miaRedemptionBalance .ccd012-redemption-mia)) + (try! (contract-call? .ccd002-treasury-nyc-mining-v2 withdraw-stx nycRedemptionBalance .ccd012-redemption-nyc)) + + ;; 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))) + (ok true) + ) +) + +(define-public (vote-on-proposal (vote bool)) + (let + ( + (voterId (unwrap! (contract-call? .ccd003-user-registry get-user-id contract-caller) ERR_USER_NOT_FOUND)) + (voterRecord (map-get? UserVotes voterId)) + ) + ;; check if vote is active + (asserts! (var-get voteActive) ERR_PROPOSAL_NOT_ACTIVE) + ;; check if vote record exists for user + (match voterRecord record + ;; if the voterRecord exists + (let + ( + (oldVote (get vote record)) + (miaVoteAmount (get mia record)) + (nycVoteAmount (get nyc record)) + ) + ;; check vote is not the same as before + (asserts! (not (is-eq oldVote vote)) ERR_VOTED_ALREADY) + ;; record the new vote for the user + (map-set UserVotes voterId + (merge record { vote: vote }) + ) + ;; update vote stats for each city + (update-city-votes MIA_ID miaVoteAmount vote true) + (update-city-votes NYC_ID nycVoteAmount vote true) + (ok true) + ) + ;; if the voterRecord does not exist + (let + ( + (miaVoteAmount (scale-down (default-to u0 (get-mia-vote voterId true)))) + (nycVoteAmount (scale-down (default-to u0 (get-nyc-vote voterId true)))) + ) + ;; check that the user has a positive vote + (asserts! (or (> miaVoteAmount u0) (> nycVoteAmount u0)) ERR_NOTHING_STACKED) + ;; insert new user vote record + (map-insert UserVotes voterId { + vote: vote, + mia: miaVoteAmount, + nyc: nycVoteAmount + }) + ;; update vote stats for each city + (update-city-votes MIA_ID miaVoteAmount vote false) + (update-city-votes NYC_ID nycVoteAmount vote false) + (ok true) + ) + ) + ) +) + +;; READ ONLY FUNCTIONS + +(define-read-only (is-executable) + (let + ( + (votingRecord (unwrap! (get-vote-totals) ERR_PANIC)) + (miaRecord (get mia votingRecord)) + (nycRecord (get nyc votingRecord)) + (voteTotals (get totals votingRecord)) + ) + ;; check that there is at least one vote + (asserts! (or (> (get totalVotesYes voteTotals) u0) (> (get totalVotesNo voteTotals) u0)) ERR_VOTE_FAILED) + ;; check that the yes total is more than no total + (asserts! (> (get totalVotesYes voteTotals) (get totalVotesNo voteTotals)) ERR_VOTE_FAILED) + ;; check that each city has at least 25% of the total "yes" votes + (asserts! (and + (>= (get totalAmountYes miaRecord) (/ (get totalAmountYes voteTotals) u4)) + (>= (get totalAmountYes nycRecord) (/ (get totalAmountYes voteTotals) u4)) + ) ERR_VOTE_FAILED) + ;; allow execution + (ok true) + ) +) + +(define-read-only (is-vote-active) + (some (var-get voteActive)) +) + +(define-read-only (get-proposal-info) + (some CCIP_020) +) + +(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-total-mia) + (map-get? CityVotes MIA_ID) +) + +(define-read-only (get-vote-total-mia-or-default) + (default-to { totalAmountYes: u0, totalAmountNo: u0, totalVotesYes: u0, totalVotesNo: u0 } (get-vote-total-mia)) +) + +(define-read-only (get-vote-total-nyc) + (map-get? CityVotes NYC_ID) +) + +(define-read-only (get-vote-total-nyc-or-default) + (default-to { totalAmountYes: u0, totalAmountNo: u0, totalVotesYes: u0, totalVotesNo: u0 } (get-vote-total-nyc)) +) + +(define-read-only (get-vote-totals) + (let + ( + (miaRecord (get-vote-total-mia-or-default)) + (nycRecord (get-vote-total-nyc-or-default)) + ) + (some { + mia: miaRecord, + nyc: nycRecord, + totals: { + totalAmountYes: (+ (get totalAmountYes miaRecord) (get totalAmountYes nycRecord)), + totalAmountNo: (+ (get totalAmountNo miaRecord) (get totalAmountNo nycRecord)), + totalVotesYes: (+ (get totalVotesYes miaRecord) (get totalVotesYes nycRecord)), + totalVotesNo: (+ (get totalVotesNo miaRecord) (get totalVotesNo nycRecord)), + } + }) + ) +) + +(define-read-only (get-voter-info (id uint)) + (map-get? UserVotes id) +) + +;; MIA vote calculation +;; returns (some uint) or (none) +;; optionally scaled by VOTE_SCALE_FACTOR (10^6) +(define-read-only (get-mia-vote (userId uint) (scaled bool)) + (let + ( + ;; MAINNET: MIA cycle 80 / first block BTC 834,050 STX 142,301 + ;; cycle 2 / u4500 used in tests + (cycle80Hash (unwrap! (get-block-hash u4500) none)) + (cycle80Data (at-block cycle80Hash (contract-call? .ccd007-citycoin-stacking get-stacker MIA_ID u2 userId))) + (cycle80Amount (get stacked cycle80Data)) + ;; MAINNET: MIA cycle 81 / first block BTC 836,150 STX 143,989 + ;; cycle 3 / u6600 used in tests + (cycle81Hash (unwrap! (get-block-hash u6600) none)) + (cycle81Data (at-block cycle81Hash (contract-call? .ccd007-citycoin-stacking get-stacker MIA_ID u3 userId))) + (cycle81Amount (get stacked cycle81Data)) + ;; MIA vote calculation + (avgStacked (/ (+ (scale-up cycle80Amount) (scale-up cycle81Amount)) u2)) + (scaledVote (/ (* avgStacked MIA_SCALE_FACTOR) MIA_SCALE_BASE)) + ) + ;; check that at least one value is positive + (asserts! (or (> cycle80Amount u0) (> cycle81Amount u0)) none) + ;; return scaled or unscaled value + (if scaled (some scaledVote) (some (/ scaledVote VOTE_SCALE_FACTOR))) + ) +) + +;; NYC vote calculation +;; returns (some uint) or (none) +;; optionally scaled by VOTE_SCALE_FACTOR (10^6) +(define-read-only (get-nyc-vote (userId uint) (scaled bool)) + (let + ( + ;; NYC cycle 80 / first block BTC 834,050 STX 142,301 + ;; cycle 2 / u4500 used in tests + (cycle80Hash (unwrap! (get-block-hash u4500) none)) + (cycle80Data (at-block cycle80Hash (contract-call? .ccd007-citycoin-stacking get-stacker NYC_ID u2 userId))) + (cycle80Amount (get stacked cycle80Data)) + ;; NYC cycle 81 / first block BTC 836,150 STX 143,989 + ;; cycle 3 / u6600 used in tests + (cycle81Hash (unwrap! (get-block-hash u6600) none)) + (cycle81Data (at-block cycle81Hash (contract-call? .ccd007-citycoin-stacking get-stacker NYC_ID u3 userId))) + (cycle81Amount (get stacked cycle81Data)) + ;; NYC vote calculation + (scaledVote (/ (+ (scale-up cycle80Amount) (scale-up cycle81Amount)) u2)) + ) + ;; check that at least one value is positive + (asserts! (or (> cycle80Amount u0) (> cycle81Amount u0)) none) + ;; return scaled or unscaled value + (if scaled (some scaledVote) (some (/ scaledVote VOTE_SCALE_FACTOR))) + ) +) + +;; PRIVATE FUNCTIONS + +;; update city vote map +(define-private (update-city-votes (cityId uint) (voteAmount uint) (vote bool) (changedVote bool)) + (let + ( + (cityRecord (default-to + { totalAmountYes: u0, totalAmountNo: u0, totalVotesYes: u0, totalVotesNo: u0 } + (map-get? CityVotes cityId))) + ) + ;; do not record if amount is 0 + (asserts! (> voteAmount u0) false) + ;; handle vote + (if vote + ;; handle yes vote + (map-set CityVotes cityId { + totalAmountYes: (+ voteAmount (get totalAmountYes cityRecord)), + totalVotesYes: (+ u1 (get totalVotesYes cityRecord)), + totalAmountNo: (if changedVote (- (get totalAmountNo cityRecord) voteAmount) (get totalAmountNo cityRecord)), + totalVotesNo: (if changedVote (- (get totalVotesNo cityRecord) u1) (get totalVotesNo cityRecord)) + }) + ;; handle no vote + (map-set CityVotes cityId { + totalAmountYes: (if changedVote (- (get totalAmountYes cityRecord) voteAmount) (get totalAmountYes cityRecord)), + totalVotesYes: (if changedVote (- (get totalVotesYes cityRecord) u1) (get totalVotesYes cityRecord)), + totalAmountNo: (+ voteAmount (get totalAmountNo cityRecord)), + totalVotesNo: (+ u1 (get totalVotesNo cityRecord)), + }) + ) + ) +) + +;; get block hash by height +(define-private (get-block-hash (blockHeight uint)) + (get-block-info? id-header-hash blockHeight) +) + +;; CREDIT: ALEX math-fixed-point-16.clar + +(define-private (scale-up (a uint)) + (* a VOTE_SCALE_FACTOR) +) + +(define-private (scale-down (a uint)) + (/ a VOTE_SCALE_FACTOR) +) From 92e3f33e08c7dbf3ccf5e2a6fc35e6b3734d7993 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Sat, 4 May 2024 07:55:04 -0700 Subject: [PATCH 02/65] fix: add draft of redemption contract from ccip-020 --- .../extensions/ccd012-redemtpion-mia.clar | 186 ++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 contracts/extensions/ccd012-redemtpion-mia.clar diff --git a/contracts/extensions/ccd012-redemtpion-mia.clar b/contracts/extensions/ccd012-redemtpion-mia.clar new file mode 100644 index 0000000..a399db0 --- /dev/null +++ b/contracts/extensions/ccd012-redemtpion-mia.clar @@ -0,0 +1,186 @@ +;; Title: CCD012 - CityCoin Redemption (MIA) +;; Version: 1.0.0 +;; Summary: A redemption extension that allows users to redeem CityCoins for a portion of the city treasury. +;; Description: An extension that provides the ability to claim a portion of the city treasury in exchange for CityCoins. + +;; TRAITS + +(impl-trait .extension-trait.extension-trait) + +;; CONSTANTS + +;; error codes +(define-constant ERR_UNAUTHORIZED (err u12000)) +(define-constant ERR_PANIC (err u12001)) +(define-constant ERR_GETTING_TOTAL_SUPPLY (err u12002)) +(define-constant ERR_GETTING_REDEMPTION_BALANCE (err u12003)) +(define-constant ERR_ALREADY_ENABLED (err u12004)) +(define-constant ERR_NOT_ENABLED (err u12005)) +(define-constant ERR_BALANCE_NOT_FOUND (err u12006)) +(define-constant ERR_NOTHING_TO_REDEEM (err u12007)) + +;; helpers +(define-constant MICRO_CITYCOINS (pow u10 u6)) ;; 6 decimal places + +;; DATA VARS + +(define-data-var redemptionsEnabled bool false) +(define-data-var blockHeight uint u0) +(define-data-var totalSupply uint u0) +(define-data-var contractBalance uint u0) +(define-data-var redemptionRatio uint u0) + +;; DATA MAPS + +;; track totals per principal +(define-map RedemptionClaims + principal ;; address + uint ;; total redemption amount +) + +;; 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) +) + +;; initialize contract after deployment to start redemptions +(define-public (initialize-redemptions) + (let + ( + ;; MAINNET: TODO + (miaTotalSupplyV1 (unwrap! (contract-call? .miamicoin-token get-total-supply) ERR_PANIC)) + (miaTotalSupplyV2 (unwrap! (contract-call? .miamicoin-token-v2 get-total-supply) ERR_PANIC)) + (miaTotalSupply (+ (* miaTotalSupplyV1 MICRO_CITYCOINS) miaTotalSupplyV2)) + (miaRedemptionBalance (as-contract (stx-get-balance tx-sender))) + ) + ;; check if sender is DAO or extension + (try! (is-dao-or-extension)) + ;; check that total supply is greater than 0 + (asserts! (and (> miaTotalSupplyV1 u0) (> miaTotalSupplyV2 u0)) ERR_GETTING_TOTAL_SUPPLY) + ;; check that redemption balance is greater than 0 + (asserts! (> miaRedemptionBalance u0) ERR_GETTING_REDEMPTION_BALANCE) + ;; check if redemptions are already enabled + (asserts! (not (var-get redemptionsEnabled)) ERR_ALREADY_ENABLED) + ;; record current block height + (var-set blockHeight block-height) + ;; record total supply at block height + (var-set totalSupply miaTotalSupply) + ;; record contract balance at block height + (var-set contractBalance miaRedemptionBalance) + ;; calculate redemption ratio + (var-set redemptionRatio (/ miaRedemptionBalance miaTotalSupply)) + ;; set redemptionsEnabled to true, can only run once + (ok (var-set redemptionsEnabled true)) + ) +) + +(define-public (redeem-mia) + (let + ( + (userAddress tx-sender) + (balanceV1 (unwrap! (contract-call? .miamicoin-token get-balance userAddress) ERR_BALANCE_NOT_FOUND)) + (balanceV2 (unwrap! (contract-call? .miamicoin-token-v2 get-balance userAddress) ERR_BALANCE_NOT_FOUND)) + (totalBalance (+ (* balanceV1 MICRO_CITYCOINS) balanceV2)) + (redemptionAmount (unwrap! (get-redemption-for-balance totalBalance) ERR_NOTHING_TO_REDEEM)) + (redemptionClaims (default-to u0 (get-redemption-amount-claimed userAddress))) + ) + ;; check if redemptions are enabled + (asserts! (var-get redemptionsEnabled) ERR_NOT_ENABLED) + ;; check that redemption amount is > 0 + (asserts! (> redemptionAmount u0) ERR_NOTHING_TO_REDEEM) + ;; burn MIA + (and (> u0 balanceV1) (try! (contract-call? .miamicoin-core-v1-patch burn-mia-v1 balanceV1 userAddress))) + (and (> u0 balanceV2) (try! (contract-call? .miamicoin-token-v2 burn balanceV2 userAddress))) + ;; transfer STX + (try! (as-contract (stx-transfer? redemptionAmount tx-sender userAddress))) + ;; update redemption claims + (map-set RedemptionClaims userAddress (+ redemptionClaims redemptionAmount)) + ;; return redemption amount + (ok redemptionAmount) + ) +) + +;; READ ONLY FUNCTIONS + +(define-read-only (is-redemption-enabled) + (var-get redemptionsEnabled) +) + +(define-read-only (get-redemption-block-height) + (var-get blockHeight) +) + +(define-read-only (get-redemption-total-supply) + (var-get totalSupply) +) + +(define-read-only (get-redemption-contract-balance) + (var-get contractBalance) +) + +(define-read-only (get-redemption-ratio) + (var-get redemptionRatio) +) + +;; aggregate all exposed vars above +(define-read-only (get-redemption-info) + { + redemptionsEnabled: (var-get redemptionsEnabled), + blockHeight: (var-get blockHeight), + totalSupply: (var-get totalSupply), + contractBalance: (var-get contractBalance), + redemptionRatio: (var-get redemptionRatio) + } +) + +(define-read-only (get-mia-balances) + (let + ( + (balanceV1 (unwrap! (contract-call? .miamicoin-token get-balance tx-sender) ERR_BALANCE_NOT_FOUND)) + (balanceV2 (unwrap! (contract-call? .miamicoin-token-v2 get-balance tx-sender) ERR_BALANCE_NOT_FOUND)) + (totalBalance (+ (* balanceV1 MICRO_CITYCOINS) balanceV2)) + ) + (ok { + balanceV1: balanceV1, + balanceV2: balanceV2, + totalBalance: totalBalance + }) + ) +) + +(define-read-only (get-redemption-for-balance (balance uint)) + (begin + (asserts! (var-get redemptionsEnabled) none) + (some (* balance (var-get redemptionRatio))) + ) +) + +(define-read-only (get-redemption-amount-claimed (address principal)) + (map-get? RedemptionClaims tx-sender) +) + +;; aggregate all exposed vars above +(define-read-only (get-user-redemption-info) + (let + ( + (miaBalances (try! (get-mia-balances))) + (redemptionAmount (default-to u0 (get-redemption-for-balance (get totalBalance miaBalances)))) + (redemptionClaims (default-to u0 (get-redemption-amount-claimed tx-sender))) + ) + (ok { + address: tx-sender, + miaBalances: miaBalances, + redemptionAmount: redemptionAmount, + redemptionClaims: redemptionClaims + }) + ) +) + +;; PRIVATE FUNCTIONS From 27db7bab42c46981310d9892576dbbe9ff3f3acb Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Mon, 13 May 2024 13:05:27 -0700 Subject: [PATCH 03/65] fix: update contract naming and calls for NYC --- ...on-mia.clar => ccd012-redemtpion-nyc.clar} | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) rename contracts/extensions/{ccd012-redemtpion-mia.clar => ccd012-redemtpion-nyc.clar} (75%) diff --git a/contracts/extensions/ccd012-redemtpion-mia.clar b/contracts/extensions/ccd012-redemtpion-nyc.clar similarity index 75% rename from contracts/extensions/ccd012-redemtpion-mia.clar rename to contracts/extensions/ccd012-redemtpion-nyc.clar index a399db0..0bf54c8 100644 --- a/contracts/extensions/ccd012-redemtpion-mia.clar +++ b/contracts/extensions/ccd012-redemtpion-nyc.clar @@ -1,4 +1,4 @@ -;; Title: CCD012 - CityCoin Redemption (MIA) +;; Title: CCD012 - CityCoin Redemption (NYC) ;; Version: 1.0.0 ;; Summary: A redemption extension that allows users to redeem CityCoins for a portion of the city treasury. ;; Description: An extension that provides the ability to claim a portion of the city treasury in exchange for CityCoins. @@ -55,38 +55,38 @@ (let ( ;; MAINNET: TODO - (miaTotalSupplyV1 (unwrap! (contract-call? .miamicoin-token get-total-supply) ERR_PANIC)) - (miaTotalSupplyV2 (unwrap! (contract-call? .miamicoin-token-v2 get-total-supply) ERR_PANIC)) - (miaTotalSupply (+ (* miaTotalSupplyV1 MICRO_CITYCOINS) miaTotalSupplyV2)) - (miaRedemptionBalance (as-contract (stx-get-balance tx-sender))) + (nycTotalSupplyV1 (unwrap! (contract-call? .newyorkcitycoin-token get-total-supply) ERR_PANIC)) + (nycTotalSupplyV2 (unwrap! (contract-call? .newyorkcitycoin-token-v2 get-total-supply) ERR_PANIC)) + (nycTotalSupply (+ (* nycTotalSupplyV1 MICRO_CITYCOINS) nycTotalSupplyV2)) + (nycRedemptionBalance (as-contract (stx-get-balance tx-sender))) ) ;; check if sender is DAO or extension (try! (is-dao-or-extension)) ;; check that total supply is greater than 0 - (asserts! (and (> miaTotalSupplyV1 u0) (> miaTotalSupplyV2 u0)) ERR_GETTING_TOTAL_SUPPLY) + (asserts! (and (> nycTotalSupplyV1 u0) (> nycTotalSupplyV2 u0)) ERR_GETTING_TOTAL_SUPPLY) ;; check that redemption balance is greater than 0 - (asserts! (> miaRedemptionBalance u0) ERR_GETTING_REDEMPTION_BALANCE) + (asserts! (> nycRedemptionBalance u0) ERR_GETTING_REDEMPTION_BALANCE) ;; check if redemptions are already enabled (asserts! (not (var-get redemptionsEnabled)) ERR_ALREADY_ENABLED) ;; record current block height (var-set blockHeight block-height) ;; record total supply at block height - (var-set totalSupply miaTotalSupply) + (var-set totalSupply nycTotalSupply) ;; record contract balance at block height - (var-set contractBalance miaRedemptionBalance) + (var-set contractBalance nycRedemptionBalance) ;; calculate redemption ratio - (var-set redemptionRatio (/ miaRedemptionBalance miaTotalSupply)) + (var-set redemptionRatio (/ nycRedemptionBalance nycTotalSupply)) ;; set redemptionsEnabled to true, can only run once (ok (var-set redemptionsEnabled true)) ) ) -(define-public (redeem-mia) +(define-public (redeem-nyc) (let ( (userAddress tx-sender) - (balanceV1 (unwrap! (contract-call? .miamicoin-token get-balance userAddress) ERR_BALANCE_NOT_FOUND)) - (balanceV2 (unwrap! (contract-call? .miamicoin-token-v2 get-balance userAddress) ERR_BALANCE_NOT_FOUND)) + (balanceV1 (unwrap! (contract-call? .newyorkcitycoin-token get-balance userAddress) ERR_BALANCE_NOT_FOUND)) + (balanceV2 (unwrap! (contract-call? .newyorkcitycoin-token-v2 get-balance userAddress) ERR_BALANCE_NOT_FOUND)) (totalBalance (+ (* balanceV1 MICRO_CITYCOINS) balanceV2)) (redemptionAmount (unwrap! (get-redemption-for-balance totalBalance) ERR_NOTHING_TO_REDEEM)) (redemptionClaims (default-to u0 (get-redemption-amount-claimed userAddress))) @@ -95,9 +95,9 @@ (asserts! (var-get redemptionsEnabled) ERR_NOT_ENABLED) ;; check that redemption amount is > 0 (asserts! (> redemptionAmount u0) ERR_NOTHING_TO_REDEEM) - ;; burn MIA - (and (> u0 balanceV1) (try! (contract-call? .miamicoin-core-v1-patch burn-mia-v1 balanceV1 userAddress))) - (and (> u0 balanceV2) (try! (contract-call? .miamicoin-token-v2 burn balanceV2 userAddress))) + ;; burn NYC + (and (> u0 balanceV1) (try! (contract-call? .newyorkcitycoin-token burn balanceV1 userAddress))) + (and (> u0 balanceV2) (try! (contract-call? .newyorkcitycoin-token-v2 burn balanceV2 userAddress))) ;; transfer STX (try! (as-contract (stx-transfer? redemptionAmount tx-sender userAddress))) ;; update redemption claims @@ -140,11 +140,11 @@ } ) -(define-read-only (get-mia-balances) +(define-read-only (get-nyc-balances) (let ( - (balanceV1 (unwrap! (contract-call? .miamicoin-token get-balance tx-sender) ERR_BALANCE_NOT_FOUND)) - (balanceV2 (unwrap! (contract-call? .miamicoin-token-v2 get-balance tx-sender) ERR_BALANCE_NOT_FOUND)) + (balanceV1 (unwrap! (contract-call? .newyorkcitycoin-token get-balance tx-sender) ERR_BALANCE_NOT_FOUND)) + (balanceV2 (unwrap! (contract-call? .newyorkcitycoin-token-v2 get-balance tx-sender) ERR_BALANCE_NOT_FOUND)) (totalBalance (+ (* balanceV1 MICRO_CITYCOINS) balanceV2)) ) (ok { @@ -170,13 +170,13 @@ (define-read-only (get-user-redemption-info) (let ( - (miaBalances (try! (get-mia-balances))) - (redemptionAmount (default-to u0 (get-redemption-for-balance (get totalBalance miaBalances)))) + (nycBalances (try! (get-nyc-balances))) + (redemptionAmount (default-to u0 (get-redemption-for-balance (get totalBalance nycBalances)))) (redemptionClaims (default-to u0 (get-redemption-amount-claimed tx-sender))) ) (ok { address: tx-sender, - miaBalances: miaBalances, + nycBalances: nycBalances, redemptionAmount: redemptionAmount, redemptionClaims: redemptionClaims }) From 05389b57e67f0dad1f3cccd937596b86995fd65c Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Mon, 13 May 2024 13:17:42 -0700 Subject: [PATCH 04/65] fix: update contract for just nyc redemptions --- ...22-citycoins-treasury-redemption-nyc.clar} | 117 ++++-------------- 1 file changed, 25 insertions(+), 92 deletions(-) rename contracts/proposals/{ccip022-citycoins-treasury-redemption.clar => ccip022-citycoins-treasury-redemption-nyc.clar} (57%) diff --git a/contracts/proposals/ccip022-citycoins-treasury-redemption.clar b/contracts/proposals/ccip022-citycoins-treasury-redemption-nyc.clar similarity index 57% rename from contracts/proposals/ccip022-citycoins-treasury-redemption.clar rename to contracts/proposals/ccip022-citycoins-treasury-redemption-nyc.clar index b547c91..8538080 100644 --- a/contracts/proposals/ccip022-citycoins-treasury-redemption.clar +++ b/contracts/proposals/ccip022-citycoins-treasury-redemption-nyc.clar @@ -15,27 +15,21 @@ ;; CONSTANTS -;; used to pay out missed cycles in protocol -(define-constant MISSED_PAYOUT u1) - (define-constant SELF (as-contract tx-sender)) (define-constant CCIP_022 { - name: "CityCoins Treasury Redemption", - link: "https://github.com/citycoins/governance/blob/feat/add-ccip-022/ccips/ccip-022/ccip-022-citycoins-treasury-redemption.md", + name: "CityCoins Treasury Redemption (NYC)", + link: "https://github.com/citycoins/governance/blob/feat/add-ccip-022/ccips/ccip-022/ccip-022-citycoins-treasury-redemption-nyc.md", hash: "TBD", }) -(define-constant MIA_ID u1) ;; (contract-call? .ccd004-city-registry get-city-id "mia") -(define-constant NYC_ID u2) ;; (contract-call? .ccd004-city-registry get-city-id "nyc") - (define-constant VOTE_SCALE_FACTOR (pow u10 u16)) ;; 16 decimal places -(define-constant MIA_SCALE_BASE (pow u10 u4)) ;; 4 decimal places -(define-constant MIA_SCALE_FACTOR u8916) ;; 0.8916 or 89.16% -;; MIA votes scaled to make 1 MIA = 1 NYC -;; full calculation available in CCIP-022 ;; DATA VARS +;; set city ID, fail if not found +(define-data-var nycId uint u0) +(var-set nycId (unwrap! (contract-call? .ccd004-city-registry get-city-id "nyc") ERR_PANIC)) + ;; vote block heights (define-data-var voteActive bool true) (define-data-var voteStart uint u0) @@ -76,23 +70,8 @@ ;; transfer funds to new redemption extensions (try! (contract-call? .ccd002-treasury-mia-mining-v2 withdraw-stx miaRedemptionBalance .ccd012-redemption-mia)) (try! (contract-call? .ccd002-treasury-nyc-mining-v2 withdraw-stx nycRedemptionBalance .ccd012-redemption-nyc)) - - ;; 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))) + ;; initialize redemption extension + (try! (contract-call? .ccd012-redemption-mia initialize-redemptions)) (ok true) ) ) @@ -111,7 +90,6 @@ (let ( (oldVote (get vote record)) - (miaVoteAmount (get mia record)) (nycVoteAmount (get nyc record)) ) ;; check vote is not the same as before @@ -121,27 +99,23 @@ (merge record { vote: vote }) ) ;; update vote stats for each city - (update-city-votes MIA_ID miaVoteAmount vote true) - (update-city-votes NYC_ID nycVoteAmount vote true) + (update-city-votes (var-get nycId) nycVoteAmount vote true) (ok true) ) ;; if the voterRecord does not exist (let ( - (miaVoteAmount (scale-down (default-to u0 (get-mia-vote voterId true)))) (nycVoteAmount (scale-down (default-to u0 (get-nyc-vote voterId true)))) ) ;; check that the user has a positive vote - (asserts! (or (> miaVoteAmount u0) (> nycVoteAmount u0)) ERR_NOTHING_STACKED) + (asserts! (or (> nycVoteAmount u0)) ERR_NOTHING_STACKED) ;; insert new user vote record (map-insert UserVotes voterId { vote: vote, - mia: miaVoteAmount, nyc: nycVoteAmount }) ;; update vote stats for each city - (update-city-votes MIA_ID miaVoteAmount vote false) - (update-city-votes NYC_ID nycVoteAmount vote false) + (update-city-votes (var-get nycId) nycVoteAmount vote false) (ok true) ) ) @@ -154,7 +128,6 @@ (let ( (votingRecord (unwrap! (get-vote-totals) ERR_PANIC)) - (miaRecord (get mia votingRecord)) (nycRecord (get nyc votingRecord)) (voteTotals (get totals votingRecord)) ) @@ -163,10 +136,7 @@ ;; check that the yes total is more than no total (asserts! (> (get totalVotesYes voteTotals) (get totalVotesNo voteTotals)) ERR_VOTE_FAILED) ;; check that each city has at least 25% of the total "yes" votes - (asserts! (and - (>= (get totalAmountYes miaRecord) (/ (get totalAmountYes voteTotals) u4)) - (>= (get totalAmountYes nycRecord) (/ (get totalAmountYes voteTotals) u4)) - ) ERR_VOTE_FAILED) + (asserts! (>= (and (get totalAmountYes nycRecord) (/ (get totalAmountYes voteTotals) u4))) ERR_VOTE_FAILED) ;; allow execution (ok true) ) @@ -195,16 +165,8 @@ ) ) -(define-read-only (get-vote-total-mia) - (map-get? CityVotes MIA_ID) -) - -(define-read-only (get-vote-total-mia-or-default) - (default-to { totalAmountYes: u0, totalAmountNo: u0, totalVotesYes: u0, totalVotesNo: u0 } (get-vote-total-mia)) -) - (define-read-only (get-vote-total-nyc) - (map-get? CityVotes NYC_ID) + (map-get? CityVotes (var-get nycId)) ) (define-read-only (get-vote-total-nyc-or-default) @@ -214,17 +176,15 @@ (define-read-only (get-vote-totals) (let ( - (miaRecord (get-vote-total-mia-or-default)) (nycRecord (get-vote-total-nyc-or-default)) ) (some { - mia: miaRecord, nyc: nycRecord, totals: { - totalAmountYes: (+ (get totalAmountYes miaRecord) (get totalAmountYes nycRecord)), - totalAmountNo: (+ (get totalAmountNo miaRecord) (get totalAmountNo nycRecord)), - totalVotesYes: (+ (get totalVotesYes miaRecord) (get totalVotesYes nycRecord)), - totalVotesNo: (+ (get totalVotesNo miaRecord) (get totalVotesNo nycRecord)), + totalAmountYes: (get totalAmountYes nycRecord), + totalAmountNo: (get totalAmountNo nycRecord), + totalVotesYes: (get totalVotesYes nycRecord), + totalVotesNo: (get totalVotesNo nycRecord), } }) ) @@ -234,49 +194,22 @@ (map-get? UserVotes id) ) -;; MIA vote calculation -;; returns (some uint) or (none) -;; optionally scaled by VOTE_SCALE_FACTOR (10^6) -(define-read-only (get-mia-vote (userId uint) (scaled bool)) - (let - ( - ;; MAINNET: MIA cycle 80 / first block BTC 834,050 STX 142,301 - ;; cycle 2 / u4500 used in tests - (cycle80Hash (unwrap! (get-block-hash u4500) none)) - (cycle80Data (at-block cycle80Hash (contract-call? .ccd007-citycoin-stacking get-stacker MIA_ID u2 userId))) - (cycle80Amount (get stacked cycle80Data)) - ;; MAINNET: MIA cycle 81 / first block BTC 836,150 STX 143,989 - ;; cycle 3 / u6600 used in tests - (cycle81Hash (unwrap! (get-block-hash u6600) none)) - (cycle81Data (at-block cycle81Hash (contract-call? .ccd007-citycoin-stacking get-stacker MIA_ID u3 userId))) - (cycle81Amount (get stacked cycle81Data)) - ;; MIA vote calculation - (avgStacked (/ (+ (scale-up cycle80Amount) (scale-up cycle81Amount)) u2)) - (scaledVote (/ (* avgStacked MIA_SCALE_FACTOR) MIA_SCALE_BASE)) - ) - ;; check that at least one value is positive - (asserts! (or (> cycle80Amount u0) (> cycle81Amount u0)) none) - ;; return scaled or unscaled value - (if scaled (some scaledVote) (some (/ scaledVote VOTE_SCALE_FACTOR))) - ) -) - ;; NYC vote calculation ;; returns (some uint) or (none) ;; optionally scaled by VOTE_SCALE_FACTOR (10^6) (define-read-only (get-nyc-vote (userId uint) (scaled bool)) (let ( - ;; NYC cycle 80 / first block BTC 834,050 STX 142,301 + ;; NYC cycle 82 / first block BTC 838,250 STX 145,643 ;; cycle 2 / u4500 used in tests - (cycle80Hash (unwrap! (get-block-hash u4500) none)) - (cycle80Data (at-block cycle80Hash (contract-call? .ccd007-citycoin-stacking get-stacker NYC_ID u2 userId))) - (cycle80Amount (get stacked cycle80Data)) - ;; NYC cycle 81 / first block BTC 836,150 STX 143,989 + (cycle82Hash (unwrap! (get-block-hash u4500) none)) + (cycle82Data (at-block cycle80Hash (contract-call? .ccd007-citycoin-stacking get-stacker nycId u2 userId))) + (cycle82Amount (get stacked cycle80Data)) + ;; NYC cycle 83 / first block BTC 840,350 STX 147,282 ;; cycle 3 / u6600 used in tests - (cycle81Hash (unwrap! (get-block-hash u6600) none)) - (cycle81Data (at-block cycle81Hash (contract-call? .ccd007-citycoin-stacking get-stacker NYC_ID u3 userId))) - (cycle81Amount (get stacked cycle81Data)) + (cycle83Hash (unwrap! (get-block-hash u6600) none)) + (cycle83Data (at-block cycle81Hash (contract-call? .ccd007-citycoin-stacking get-stacker nycId u3 userId))) + (cycle83Amount (get stacked cycle81Data)) ;; NYC vote calculation (scaledVote (/ (+ (scale-up cycle80Amount) (scale-up cycle81Amount)) u2)) ) From 87169d51c58ccb9b75ac387d66a2b7c14bea0814 Mon Sep 17 00:00:00 2001 From: friedger Date: Tue, 14 May 2024 10:40:39 +0200 Subject: [PATCH 05/65] fix: correct syntax errors --- Clarinet.toml | 10 +++ ...on-nyc.clar => ccd012-redemption-nyc.clar} | 0 ...022-citycoins-treasury-redemption-nyc.clar | 78 +++++++++---------- 3 files changed, 48 insertions(+), 40 deletions(-) rename contracts/extensions/{ccd012-redemtpion-nyc.clar => ccd012-redemption-nyc.clar} (100%) diff --git a/Clarinet.toml b/Clarinet.toml index 56ef02f..58bd761 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -140,6 +140,11 @@ path = "contracts/extensions/ccd010-core-v2-adapter.clar" [contracts.ccd011-stacking-payouts] path = "contracts/extensions/ccd011-stacking-payouts.clar" +[contracts.ccd012-redemption-nyc] +path = "contracts/extensions/ccd012-redemption-nyc.clar" +clarity_version = 2 +epoch = "2.5" + [contracts.ccip012-bootstrap] path = "contracts/proposals/ccip012-bootstrap.clar" @@ -174,6 +179,11 @@ path = "contracts/proposals/ccip021-extend-sunset-period-2.clar" clarity_version = 2 epoch = 2.4 +[contracts.ccip022-citycoins-treasury-redemption-nyc] +path = "contracts/proposals/ccip022-citycoins-treasury-redemption-nyc.clar" +clarity_version = 2 +epoch = 2.5 + # CITYCOINS PROTOCOL TRAITS [contracts.extension-trait] diff --git a/contracts/extensions/ccd012-redemtpion-nyc.clar b/contracts/extensions/ccd012-redemption-nyc.clar similarity index 100% rename from contracts/extensions/ccd012-redemtpion-nyc.clar rename to contracts/extensions/ccd012-redemption-nyc.clar diff --git a/contracts/proposals/ccip022-citycoins-treasury-redemption-nyc.clar b/contracts/proposals/ccip022-citycoins-treasury-redemption-nyc.clar index 8538080..f4b7711 100644 --- a/contracts/proposals/ccip022-citycoins-treasury-redemption-nyc.clar +++ b/contracts/proposals/ccip022-citycoins-treasury-redemption-nyc.clar @@ -27,8 +27,7 @@ ;; DATA VARS ;; set city ID, fail if not found -(define-data-var nycId uint u0) -(var-set nycId (unwrap! (contract-call? .ccd004-city-registry get-city-id "nyc") ERR_PANIC)) +(define-constant nycId (unwrap! (contract-call? .ccd004-city-registry get-city-id "nyc") ERR_PANIC)) ;; vote block heights (define-data-var voteActive bool true) @@ -54,26 +53,23 @@ uint ;; user ID { ;; vote vote: bool, - mia: uint, nyc: uint, } ) ;; PUBLIC FUNCTIONS (define-public (execute (sender principal)) - (begin + (let ((nycRedemptionBalance (stx-get-balance .ccd002-treasury-nyc-mining))) ;; check vote is complete/passed (try! (is-executable)) ;; update vote variables (var-set voteEnd block-height) (var-set voteActive false) ;; transfer funds to new redemption extensions - (try! (contract-call? .ccd002-treasury-mia-mining-v2 withdraw-stx miaRedemptionBalance .ccd012-redemption-mia)) (try! (contract-call? .ccd002-treasury-nyc-mining-v2 withdraw-stx nycRedemptionBalance .ccd012-redemption-nyc)) ;; initialize redemption extension - (try! (contract-call? .ccd012-redemption-mia initialize-redemptions)) - (ok true) - ) + (try! (contract-call? .ccd012-redemption-nyc initialize-redemptions)) + (ok true)) ) (define-public (vote-on-proposal (vote bool)) @@ -99,7 +95,7 @@ (merge record { vote: vote }) ) ;; update vote stats for each city - (update-city-votes (var-get nycId) nycVoteAmount vote true) + (update-city-votes nycId nycVoteAmount vote true) (ok true) ) ;; if the voterRecord does not exist @@ -109,13 +105,13 @@ ) ;; check that the user has a positive vote (asserts! (or (> nycVoteAmount u0)) ERR_NOTHING_STACKED) - ;; insert new user vote record + ;; insert new user vote record (map-insert UserVotes voterId { - vote: vote, + vote: vote, nyc: nycVoteAmount }) ;; update vote stats for each city - (update-city-votes (var-get nycId) nycVoteAmount vote false) + (update-city-votes nycId nycVoteAmount vote false) (ok true) ) ) @@ -136,7 +132,7 @@ ;; check that the yes total is more than no total (asserts! (> (get totalVotesYes voteTotals) (get totalVotesNo voteTotals)) ERR_VOTE_FAILED) ;; check that each city has at least 25% of the total "yes" votes - (asserts! (>= (and (get totalAmountYes nycRecord) (/ (get totalAmountYes voteTotals) u4))) ERR_VOTE_FAILED) + (asserts! (>= (get totalAmountYes nycRecord) (/ (get totalAmountYes voteTotals) u4)) ERR_VOTE_FAILED) ;; allow execution (ok true) ) @@ -147,7 +143,7 @@ ) (define-read-only (get-proposal-info) - (some CCIP_020) + (some CCIP_022) ) (define-read-only (get-vote-period) @@ -166,7 +162,7 @@ ) (define-read-only (get-vote-total-nyc) - (map-get? CityVotes (var-get nycId)) + (map-get? CityVotes nycId) ) (define-read-only (get-vote-total-nyc-or-default) @@ -203,18 +199,18 @@ ;; NYC cycle 82 / first block BTC 838,250 STX 145,643 ;; cycle 2 / u4500 used in tests (cycle82Hash (unwrap! (get-block-hash u4500) none)) - (cycle82Data (at-block cycle80Hash (contract-call? .ccd007-citycoin-stacking get-stacker nycId u2 userId))) - (cycle82Amount (get stacked cycle80Data)) + (cycle82Data (at-block cycle82Hash (contract-call? .ccd007-citycoin-stacking get-stacker nycId u2 userId))) + (cycle82Amount (get stacked cycle82Data)) ;; NYC cycle 83 / first block BTC 840,350 STX 147,282 ;; cycle 3 / u6600 used in tests (cycle83Hash (unwrap! (get-block-hash u6600) none)) - (cycle83Data (at-block cycle81Hash (contract-call? .ccd007-citycoin-stacking get-stacker nycId u3 userId))) - (cycle83Amount (get stacked cycle81Data)) + (cycle83Data (at-block cycle83Hash (contract-call? .ccd007-citycoin-stacking get-stacker nycId u3 userId))) + (cycle83Amount (get stacked cycle83Data)) ;; NYC vote calculation - (scaledVote (/ (+ (scale-up cycle80Amount) (scale-up cycle81Amount)) u2)) + (scaledVote (/ (+ (scale-up cycle83Amount) (scale-up cycle83Amount)) u2)) ) ;; check that at least one value is positive - (asserts! (or (> cycle80Amount u0) (> cycle81Amount u0)) none) + (asserts! (or (> cycle82Amount u0) (> cycle83Amount u0)) none) ;; return scaled or unscaled value (if scaled (some scaledVote) (some (/ scaledVote VOTE_SCALE_FACTOR))) ) @@ -226,29 +222,31 @@ (define-private (update-city-votes (cityId uint) (voteAmount uint) (vote bool) (changedVote bool)) (let ( - (cityRecord (default-to + (cityRecord (default-to { totalAmountYes: u0, totalAmountNo: u0, totalVotesYes: u0, totalVotesNo: u0 } (map-get? CityVotes cityId))) ) ;; do not record if amount is 0 - (asserts! (> voteAmount u0) false) - ;; handle vote - (if vote - ;; handle yes vote - (map-set CityVotes cityId { - totalAmountYes: (+ voteAmount (get totalAmountYes cityRecord)), - totalVotesYes: (+ u1 (get totalVotesYes cityRecord)), - totalAmountNo: (if changedVote (- (get totalAmountNo cityRecord) voteAmount) (get totalAmountNo cityRecord)), - totalVotesNo: (if changedVote (- (get totalVotesNo cityRecord) u1) (get totalVotesNo cityRecord)) - }) - ;; handle no vote - (map-set CityVotes cityId { - totalAmountYes: (if changedVote (- (get totalAmountYes cityRecord) voteAmount) (get totalAmountYes cityRecord)), - totalVotesYes: (if changedVote (- (get totalVotesYes cityRecord) u1) (get totalVotesYes cityRecord)), - totalAmountNo: (+ voteAmount (get totalAmountNo cityRecord)), - totalVotesNo: (+ u1 (get totalVotesNo cityRecord)), - }) - ) + (if (> voteAmount u0) + ;; handle vote + (if vote + ;; handle yes vote + (map-set CityVotes cityId { + totalAmountYes: (+ voteAmount (get totalAmountYes cityRecord)), + totalVotesYes: (+ u1 (get totalVotesYes cityRecord)), + totalAmountNo: (if changedVote (- (get totalAmountNo cityRecord) voteAmount) (get totalAmountNo cityRecord)), + totalVotesNo: (if changedVote (- (get totalVotesNo cityRecord) u1) (get totalVotesNo cityRecord)) + }) + ;; handle no vote + (map-set CityVotes cityId { + totalAmountYes: (if changedVote (- (get totalAmountYes cityRecord) voteAmount) (get totalAmountYes cityRecord)), + totalVotesYes: (if changedVote (- (get totalVotesYes cityRecord) u1) (get totalVotesYes cityRecord)), + totalAmountNo: (+ voteAmount (get totalAmountNo cityRecord)), + totalVotesNo: (+ u1 (get totalVotesNo cityRecord)), + }) + ) + ;; ignore calls with vote amount equal to 0 + false) ) ) From 106794ca6a77b7d2ad7cfe930af5c9ed44f6c057 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Tue, 14 May 2024 12:34:51 -0700 Subject: [PATCH 06/65] fix: add mainnet contract addresses in comments --- contracts/extensions/ccd012-redemption-nyc.clar | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/contracts/extensions/ccd012-redemption-nyc.clar b/contracts/extensions/ccd012-redemption-nyc.clar index 0bf54c8..fc39990 100644 --- a/contracts/extensions/ccd012-redemption-nyc.clar +++ b/contracts/extensions/ccd012-redemption-nyc.clar @@ -18,6 +18,7 @@ (define-constant ERR_NOT_ENABLED (err u12005)) (define-constant ERR_BALANCE_NOT_FOUND (err u12006)) (define-constant ERR_NOTHING_TO_REDEEM (err u12007)) +(define-constant ERR_ALREADY_CLAIMED (err u12008)) ;; helpers (define-constant MICRO_CITYCOINS (pow u10 u6)) ;; 6 decimal places @@ -54,7 +55,8 @@ (define-public (initialize-redemptions) (let ( - ;; MAINNET: TODO + ;; MAINNET: SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token + ;; MAINNET: SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 (nycTotalSupplyV1 (unwrap! (contract-call? .newyorkcitycoin-token get-total-supply) ERR_PANIC)) (nycTotalSupplyV2 (unwrap! (contract-call? .newyorkcitycoin-token-v2 get-total-supply) ERR_PANIC)) (nycTotalSupply (+ (* nycTotalSupplyV1 MICRO_CITYCOINS) nycTotalSupplyV2)) @@ -85,6 +87,8 @@ (let ( (userAddress tx-sender) + ;; MAINNET: SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token + ;; MAINNET: SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 (balanceV1 (unwrap! (contract-call? .newyorkcitycoin-token get-balance userAddress) ERR_BALANCE_NOT_FOUND)) (balanceV2 (unwrap! (contract-call? .newyorkcitycoin-token-v2 get-balance userAddress) ERR_BALANCE_NOT_FOUND)) (totalBalance (+ (* balanceV1 MICRO_CITYCOINS) balanceV2)) @@ -96,6 +100,8 @@ ;; check that redemption amount is > 0 (asserts! (> redemptionAmount u0) ERR_NOTHING_TO_REDEEM) ;; burn NYC + ;; MAINNET: SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token + ;; MAINNET: SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 (and (> u0 balanceV1) (try! (contract-call? .newyorkcitycoin-token burn balanceV1 userAddress))) (and (> u0 balanceV2) (try! (contract-call? .newyorkcitycoin-token-v2 burn balanceV2 userAddress))) ;; transfer STX @@ -143,6 +149,8 @@ (define-read-only (get-nyc-balances) (let ( + ;; MAINNET: SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token + ;; MAINNET: SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 (balanceV1 (unwrap! (contract-call? .newyorkcitycoin-token get-balance tx-sender) ERR_BALANCE_NOT_FOUND)) (balanceV2 (unwrap! (contract-call? .newyorkcitycoin-token-v2 get-balance tx-sender) ERR_BALANCE_NOT_FOUND)) (totalBalance (+ (* balanceV1 MICRO_CITYCOINS) balanceV2)) From 613b35b491a8a11f60e70dfe72438bb7c6c7f920 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Tue, 14 May 2024 12:41:45 -0700 Subject: [PATCH 07/65] fix: prevent multiple claims, verify balance, print info Also adds a TODO item based on new stacks-block-height keyword, requires latest version of Clarinet. --- contracts/extensions/ccd012-redemption-nyc.clar | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/contracts/extensions/ccd012-redemption-nyc.clar b/contracts/extensions/ccd012-redemption-nyc.clar index fc39990..22c45db 100644 --- a/contracts/extensions/ccd012-redemption-nyc.clar +++ b/contracts/extensions/ccd012-redemption-nyc.clar @@ -71,7 +71,7 @@ ;; check if redemptions are already enabled (asserts! (not (var-get redemptionsEnabled)) ERR_ALREADY_ENABLED) ;; record current block height - (var-set blockHeight block-height) + (var-set blockHeight block-height) ;; TODO: stacks-block-height ;; record total supply at block height (var-set totalSupply nycTotalSupply) ;; record contract balance at block height @@ -97,6 +97,10 @@ ) ;; check if redemptions are enabled (asserts! (var-get redemptionsEnabled) ERR_NOT_ENABLED) + ;; check that user has not already claimed + (asserts! (is-eq redemptionClaims u0) ERR_ALREADY_CLAIMED) + ;; check that user has at least one positive balance + (asserts! (or (> balanceV1 u0) (> balanceV2 u0)) ERR_BALANCE_NOT_FOUND) ;; check that redemption amount is > 0 (asserts! (> redemptionAmount u0) ERR_NOTHING_TO_REDEEM) ;; burn NYC @@ -108,6 +112,10 @@ (try! (as-contract (stx-transfer? redemptionAmount tx-sender userAddress))) ;; update redemption claims (map-set RedemptionClaims userAddress (+ redemptionClaims redemptionAmount)) + ;; print redemption info + (print (get-redemption-info)) + ;; print user redemption info + (print (try! (get-user-redemption-info))) ;; return redemption amount (ok redemptionAmount) ) From 551bc015683ceff460ee422e0681202c609267b0 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Tue, 14 May 2024 13:02:03 -0700 Subject: [PATCH 08/65] fix: small improvements, cleanup comments --- .../extensions/ccd012-redemption-nyc.clar | 4 +- ...022-citycoins-treasury-redemption-nyc.clar | 45 ++++++++++--------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/contracts/extensions/ccd012-redemption-nyc.clar b/contracts/extensions/ccd012-redemption-nyc.clar index 22c45db..b475646 100644 --- a/contracts/extensions/ccd012-redemption-nyc.clar +++ b/contracts/extensions/ccd012-redemption-nyc.clar @@ -52,7 +52,7 @@ ) ;; initialize contract after deployment to start redemptions -(define-public (initialize-redemptions) +(define-public (initialize-redemption) (let ( ;; MAINNET: SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token @@ -179,7 +179,7 @@ ) (define-read-only (get-redemption-amount-claimed (address principal)) - (map-get? RedemptionClaims tx-sender) + (map-get? RedemptionClaims address) ) ;; aggregate all exposed vars above diff --git a/contracts/proposals/ccip022-citycoins-treasury-redemption-nyc.clar b/contracts/proposals/ccip022-citycoins-treasury-redemption-nyc.clar index f4b7711..1f30240 100644 --- a/contracts/proposals/ccip022-citycoins-treasury-redemption-nyc.clar +++ b/contracts/proposals/ccip022-citycoins-treasury-redemption-nyc.clar @@ -6,12 +6,13 @@ ;; ERRORS (define-constant ERR_PANIC (err u2200)) -(define-constant ERR_VOTED_ALREADY (err u2201)) -(define-constant ERR_NOTHING_STACKED (err u2202)) -(define-constant ERR_USER_NOT_FOUND (err u2203)) -(define-constant ERR_PROPOSAL_NOT_ACTIVE (err u2204)) -(define-constant ERR_PROPOSAL_STILL_ACTIVE (err u2205)) -(define-constant ERR_VOTE_FAILED (err u2206)) +(define-constant ERR_SAVING_VOTE (err u2201)) +(define-constant ERR_VOTED_ALREADY (err u2202)) +(define-constant ERR_NOTHING_STACKED (err u2203)) +(define-constant ERR_USER_NOT_FOUND (err u2204)) +(define-constant ERR_PROPOSAL_NOT_ACTIVE (err u2205)) +(define-constant ERR_PROPOSAL_STILL_ACTIVE (err u2206)) +(define-constant ERR_VOTE_FAILED (err u2207)) ;; CONSTANTS @@ -24,10 +25,10 @@ (define-constant VOTE_SCALE_FACTOR (pow u10 u16)) ;; 16 decimal places -;; DATA VARS +;; set city ID +(define-constant NYC_ID u2) ;; (unwrap! (contract-call? .ccd004-city-registry get-city-id "nyc") ERR_PANIC) -;; set city ID, fail if not found -(define-constant nycId (unwrap! (contract-call? .ccd004-city-registry get-city-id "nyc") ERR_PANIC)) +;; DATA VARS ;; vote block heights (define-data-var voteActive bool true) @@ -35,6 +36,7 @@ (define-data-var voteEnd uint u0) ;; start the vote when deployed +;; TODO: stacks-block-height (var-set voteStart block-height) ;; DATA MAPS @@ -63,12 +65,13 @@ ;; check vote is complete/passed (try! (is-executable)) ;; update vote variables + ;; TODO: stacks-block-height (var-set voteEnd block-height) (var-set voteActive false) - ;; transfer funds to new redemption extensions + ;; transfer funds to new redemption extension (try! (contract-call? .ccd002-treasury-nyc-mining-v2 withdraw-stx nycRedemptionBalance .ccd012-redemption-nyc)) ;; initialize redemption extension - (try! (contract-call? .ccd012-redemption-nyc initialize-redemptions)) + (try! (contract-call? .ccd012-redemption-nyc initialize-redemption)) (ok true)) ) @@ -95,7 +98,7 @@ (merge record { vote: vote }) ) ;; update vote stats for each city - (update-city-votes nycId nycVoteAmount vote true) + (update-city-votes NYC_ID nycVoteAmount vote true) (ok true) ) ;; if the voterRecord does not exist @@ -106,12 +109,12 @@ ;; check that the user has a positive vote (asserts! (or (> nycVoteAmount u0)) ERR_NOTHING_STACKED) ;; insert new user vote record - (map-insert UserVotes voterId { + (asserts! (map-insert UserVotes voterId { vote: vote, nyc: nycVoteAmount - }) + }) ERR_SAVING_VOTE) ;; update vote stats for each city - (update-city-votes nycId nycVoteAmount vote false) + (update-city-votes NYC_ID nycVoteAmount vote false) (ok true) ) ) @@ -131,7 +134,7 @@ (asserts! (or (> (get totalVotesYes voteTotals) u0) (> (get totalVotesNo voteTotals) u0)) ERR_VOTE_FAILED) ;; check that the yes total is more than no total (asserts! (> (get totalVotesYes voteTotals) (get totalVotesNo voteTotals)) ERR_VOTE_FAILED) - ;; check that each city has at least 25% of the total "yes" votes + ;; check the "yes" votes are at least 25% of the total (asserts! (>= (get totalAmountYes nycRecord) (/ (get totalAmountYes voteTotals) u4)) ERR_VOTE_FAILED) ;; allow execution (ok true) @@ -162,7 +165,7 @@ ) (define-read-only (get-vote-total-nyc) - (map-get? CityVotes nycId) + (map-get? CityVotes NYC_ID) ) (define-read-only (get-vote-total-nyc-or-default) @@ -196,15 +199,15 @@ (define-read-only (get-nyc-vote (userId uint) (scaled bool)) (let ( - ;; NYC cycle 82 / first block BTC 838,250 STX 145,643 + ;; MAINNET: NYC cycle 82 / first block BTC 838,250 STX 145,643 ;; cycle 2 / u4500 used in tests (cycle82Hash (unwrap! (get-block-hash u4500) none)) - (cycle82Data (at-block cycle82Hash (contract-call? .ccd007-citycoin-stacking get-stacker nycId u2 userId))) + (cycle82Data (at-block cycle82Hash (contract-call? .ccd007-citycoin-stacking get-stacker NYC_ID u2 userId))) (cycle82Amount (get stacked cycle82Data)) - ;; NYC cycle 83 / first block BTC 840,350 STX 147,282 + ;; MAINNET: NYC cycle 83 / first block BTC 840,350 STX 147,282 ;; cycle 3 / u6600 used in tests (cycle83Hash (unwrap! (get-block-hash u6600) none)) - (cycle83Data (at-block cycle83Hash (contract-call? .ccd007-citycoin-stacking get-stacker nycId u3 userId))) + (cycle83Data (at-block cycle83Hash (contract-call? .ccd007-citycoin-stacking get-stacker NYC_ID u3 userId))) (cycle83Amount (get stacked cycle83Data)) ;; NYC vote calculation (scaledVote (/ (+ (scale-up cycle83Amount) (scale-up cycle83Amount)) u2)) From 6007e45acedcc3222324ec28d7c036bb57d41fbf Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Tue, 14 May 2024 13:06:51 -0700 Subject: [PATCH 09/65] fix: apply suggestions from @friedger --- contracts/extensions/ccd012-redemption-nyc.clar | 2 +- .../proposals/ccip022-citycoins-treasury-redemption-nyc.clar | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/extensions/ccd012-redemption-nyc.clar b/contracts/extensions/ccd012-redemption-nyc.clar index b475646..13347c7 100644 --- a/contracts/extensions/ccd012-redemption-nyc.clar +++ b/contracts/extensions/ccd012-redemption-nyc.clar @@ -65,7 +65,7 @@ ;; check if sender is DAO or extension (try! (is-dao-or-extension)) ;; check that total supply is greater than 0 - (asserts! (and (> nycTotalSupplyV1 u0) (> nycTotalSupplyV2 u0)) ERR_GETTING_TOTAL_SUPPLY) + (asserts! (or (> nycTotalSupplyV1 u0) (> nycTotalSupplyV2 u0)) ERR_GETTING_TOTAL_SUPPLY) ;; check that redemption balance is greater than 0 (asserts! (> nycRedemptionBalance u0) ERR_GETTING_REDEMPTION_BALANCE) ;; check if redemptions are already enabled diff --git a/contracts/proposals/ccip022-citycoins-treasury-redemption-nyc.clar b/contracts/proposals/ccip022-citycoins-treasury-redemption-nyc.clar index 1f30240..5ed08fa 100644 --- a/contracts/proposals/ccip022-citycoins-treasury-redemption-nyc.clar +++ b/contracts/proposals/ccip022-citycoins-treasury-redemption-nyc.clar @@ -210,7 +210,7 @@ (cycle83Data (at-block cycle83Hash (contract-call? .ccd007-citycoin-stacking get-stacker NYC_ID u3 userId))) (cycle83Amount (get stacked cycle83Data)) ;; NYC vote calculation - (scaledVote (/ (+ (scale-up cycle83Amount) (scale-up cycle83Amount)) u2)) + (scaledVote (/ (+ (scale-up cycle82Amount) (scale-up cycle83Amount)) u2)) ) ;; check that at least one value is positive (asserts! (or (> cycle82Amount u0) (> cycle83Amount u0)) none) From d26ffbbf1eb29d667fa129c6cda194c069899cb4 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Tue, 14 May 2024 13:09:06 -0700 Subject: [PATCH 10/65] fix: playing with workflow and clarinet versions --- .github/workflows/test-contracts.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-contracts.yaml b/.github/workflows/test-contracts.yaml index 884badd..8b8fd9a 100644 --- a/.github/workflows/test-contracts.yaml +++ b/.github/workflows/test-contracts.yaml @@ -25,10 +25,10 @@ jobs: - name: "Checkout code" uses: actions/checkout@v4 - name: "Check contract syntax" - uses: docker://hirosystems/clarinet:1.8.0 + uses: docker://hirosystems/clarinet:2.6.0 with: args: check - - name: "Run all contract tests" + - name: "Run all contract tests (legacy)" uses: docker://hirosystems/clarinet:1.8.0 with: args: test --coverage From 61a243e1e4b0404398ac1491135e3d1212e4f5e4 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Tue, 14 May 2024 13:16:28 -0700 Subject: [PATCH 11/65] fix: update legacy tests to use older Clarinet.toml This preserves the original tests and leaves out the two new contracts that would be deployed in Stacks 2.5. Those can be tested separately using the latest version of Clarinet. --- .github/workflows/test-contracts.yaml | 2 +- Clarinet.legacy.toml | 538 ++++++++++++++++++++++++++ Clarinet.toml | 2 +- 3 files changed, 540 insertions(+), 2 deletions(-) create mode 100644 Clarinet.legacy.toml diff --git a/.github/workflows/test-contracts.yaml b/.github/workflows/test-contracts.yaml index 8b8fd9a..abce20d 100644 --- a/.github/workflows/test-contracts.yaml +++ b/.github/workflows/test-contracts.yaml @@ -31,7 +31,7 @@ jobs: - name: "Run all contract tests (legacy)" uses: docker://hirosystems/clarinet:1.8.0 with: - args: test --coverage + args: test --coverage --manifest-path Clarinet.legacy.toml - name: "Upload code coverage" uses: codecov/codecov-action@v4 with: diff --git a/Clarinet.legacy.toml b/Clarinet.legacy.toml new file mode 100644 index 0000000..56ef02f --- /dev/null +++ b/Clarinet.legacy.toml @@ -0,0 +1,538 @@ +[project] +name = "citycoins-protocol" +authors = [] +description = "" +telemetry = false +boot_contracts = ["pox", "costs-v2", "bns"] + +[project.cache_location] +path = ".requirements" + +# MAINNET REQUIREMENTS + +[[project.requirements]] +contract_id = "SP000000000000000000002Q6VF78.pox" + +[[project.requirements]] +contract_id = "SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard" + +[[project.requirements]] +contract_id = "SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait" + +[[project.requirements]] +contract_id = "SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-core-v2-trait" + +[[project.requirements]] +contract_id = "SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-vrf-v2" + +[[project.requirements]] +contract_id = "SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-auth-v2" + +[[project.requirements]] +contract_id = "SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v2" + +[[project.requirements]] +contract_id = "SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2" + +[[project.requirements]] +contract_id = "SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-auth-v2" + +[[project.requirements]] +contract_id = "SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-core-v2" + +[[project.requirements]] +contract_id = "SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2" + +# TESTNET REQUIREMENTS + +[[project.requirements]] +contract_id = "ST000000000000000000002AMW42H.pox" + +[[project.requirements]] +contract_id = "ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.sip-010-trait-ft-standard" + +[[project.requirements]] +contract_id = "ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.nft-trait" + +[[project.requirements]] +contract_id = "ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-core-v2-trait" + +[[project.requirements]] +contract_id = "ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-vrf-v2" + +[[project.requirements]] +contract_id = "ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-auth-v2" + +[[project.requirements]] +contract_id = "ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-core-v2" + +[[project.requirements]] +contract_id = "ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-token-v2" + +[[project.requirements]] +contract_id = "STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-auth-v2" + +[[project.requirements]] +contract_id = "STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-core-v2" + +[[project.requirements]] +contract_id = "STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-token-v2" + +# CITYCOINS PROTOCOL CONTRACTS + +[contracts.base-dao] +path = "contracts/base-dao.clar" + +[contracts.ccd001-direct-execute] +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-mining-v3] +# path = "contracts/extensions/ccd002-treasury-v3.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-mining-v3] +# path = "contracts/extensions/ccd002-treasury-v3.clar" + +[contracts.ccd002-treasury-nyc-stacking] +path = "contracts/extensions/ccd002-treasury.clar" + +[contracts.ccd003-user-registry] +path = "contracts/extensions/ccd003-user-registry.clar" + +[contracts.ccd004-city-registry] +path = "contracts/extensions/ccd004-city-registry.clar" + +[contracts.ccd005-city-data] +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" + +[contracts.ccd008-city-activation] +path = "contracts/extensions/ccd008-city-activation.clar" + +[contracts.ccd009-auth-v2-adapter] +path = "contracts/extensions/ccd009-auth-v2-adapter.clar" + +[contracts.ccd010-core-v2-adapter] +path = "contracts/extensions/ccd010-core-v2-adapter.clar" + +[contracts.ccd011-stacking-payouts] +path = "contracts/extensions/ccd011-stacking-payouts.clar" + +[contracts.ccip012-bootstrap] +path = "contracts/proposals/ccip012-bootstrap.clar" + +[contracts.ccip013-migration] +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 + +[contracts.ccip017-extend-sunset-period] +path = "contracts/proposals/ccip017-extend-sunset-period.clar" +clarity_version = 2 +epoch = 2.4 + +[contracts.ccip020-graceful-protocol-shutdown] +path = "contracts/proposals/ccip020-graceful-protocol-shutdown.clar" +clarity_version = 2 +epoch = 2.4 + +[contracts.ccip021-extend-sunset-period-2] +path = "contracts/proposals/ccip021-extend-sunset-period-2.clar" +clarity_version = 2 +epoch = 2.4 + +# CITYCOINS PROTOCOL TRAITS + +[contracts.extension-trait] +path = "contracts/traits/extension-trait.clar" + +[contracts.proposal-trait] +path = "contracts/traits/proposal-trait.clar" + +[contracts.stacking-trait] +path = "contracts/traits/stacking-trait.clar" + +[contracts.ccd002-trait] +path = "contracts/traits/ccd002-trait.clar" + +[contracts.ccd006-trait] +path = "contracts/traits/ccd006-trait.clar" + +[contracts.ccd007-trait] +path = "contracts/traits/ccd007-trait.clar" + +[contracts.ccip015-trait] +path = "contracts/traits/ccip015-trait.clar" + +# CITYCOINS EXTERNAL CONTRACTS + +[contracts.citycoin-vrf-v2] +path = "tests/contracts/external/citycoin-vrf-v2.clar" + +[contracts.test-ccext-governance-token-mia] +path = "tests/contracts/external/test-ccext-governance-token-mia.clar" + +[contracts.test-ccext-governance-token-nyc] +path = "tests/contracts/external/test-ccext-governance-token-nyc.clar" + +[contracts.test-ccext-nft-mia] +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-vrf] +path = "contracts/legacy/citycoin-vrf.clar" + +[contracts.citycoin-core-trait] +path = "contracts/legacy/citycoin-core-trait.clar" + +[contracts.citycoin-core-v2-trait] +path = "contracts/legacy/citycoin-core-v2-trait.clar" + +[contracts.citycoin-token-trait] +path = "contracts/legacy/citycoin-token-trait.clar" + +[contracts.citycoin-token-v2-trait] +path = "contracts/legacy/citycoin-token-v2-trait.clar" + +[contracts.miamicoin-auth] +path = "contracts/legacy/miamicoin-auth.clar" + +[contracts.miamicoin-core-v1] +path = "contracts/legacy/miamicoin-core-v1.clar" + +[contracts.miamicoin-token] +path = "contracts/legacy/miamicoin-token.clar" + +[contracts.miamicoin-core-v1-patch] +path = "contracts/legacy/miamicoin-core-v1-patch.clar" + +[contracts.miamicoin-auth-v2] +path = "contracts/legacy/miamicoin-auth-v2.clar" + +[contracts.miamicoin-core-v2] +path = "contracts/legacy/miamicoin-core-v2.clar" + +[contracts.miamicoin-token-v2] +path = "contracts/legacy/miamicoin-token-v2.clar" + +[contracts.newyorkcitycoin-auth] +path = "contracts/legacy/newyorkcitycoin-auth.clar" + +[contracts.newyorkcitycoin-core-v1] +path = "contracts/legacy/newyorkcitycoin-core-v1.clar" + +[contracts.newyorkcitycoin-token] +path = "contracts/legacy/newyorkcitycoin-token.clar" + +[contracts.newyorkcitycoin-core-v1-patch] +path = "contracts/legacy/newyorkcitycoin-core-v1-patch.clar" + +[contracts.newyorkcitycoin-auth-v2] +path = "contracts/legacy/newyorkcitycoin-auth-v2.clar" + +[contracts.newyorkcitycoin-core-v2] +path = "contracts/legacy/newyorkcitycoin-core-v2.clar" + +[contracts.newyorkcitycoin-token-v2] +path = "contracts/legacy/newyorkcitycoin-token-v2.clar" + +# CITYCOINS TEST PROPOSALS + +[contracts.test-ccd001-direct-execute-001] +path = "tests/contracts/proposals/test-ccd001-direct-execute-001.clar" + +[contracts.test-ccd001-direct-execute-002] +path = "tests/contracts/proposals/test-ccd001-direct-execute-002.clar" + +[contracts.test-ccd001-direct-execute-003] +path = "tests/contracts/proposals/test-ccd001-direct-execute-003.clar" + +[contracts.test-ccd002-treasury-001] +path = "tests/contracts/proposals/test-ccd002-treasury-001.clar" + +[contracts.test-ccd002-treasury-002] +path = "tests/contracts/proposals/test-ccd002-treasury-002.clar" + +[contracts.test-ccd002-treasury-003] +path = "tests/contracts/proposals/test-ccd002-treasury-003.clar" + +[contracts.test-ccd002-treasury-004] +path = "tests/contracts/proposals/test-ccd002-treasury-004.clar" + +[contracts.test-ccd002-treasury-005] +path = "tests/contracts/proposals/test-ccd002-treasury-005.clar" + +[contracts.test-ccd002-treasury-006] +path = "tests/contracts/proposals/test-ccd002-treasury-006.clar" + +[contracts.test-ccd002-treasury-007] +path = "tests/contracts/proposals/test-ccd002-treasury-007.clar" + +[contracts.test-ccd002-treasury-008] +path = "tests/contracts/proposals/test-ccd002-treasury-008.clar" + +[contracts.test-ccd002-treasury-009] +path = "tests/contracts/proposals/test-ccd002-treasury-009.clar" + +[contracts.test-ccd002-treasury-010] +path = "tests/contracts/proposals/test-ccd002-treasury-010.clar" + +[contracts.test-ccd002-treasury-011] +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" + +[contracts.test-ccd003-user-registry-002] +path = "tests/contracts/proposals/test-ccd003-user-registry-002.clar" + +[contracts.test-ccd003-user-registry-003] +path = "tests/contracts/proposals/test-ccd003-user-registry-003.clar" + +[contracts.test-ccd004-city-registry-001] +path = "tests/contracts/proposals/test-ccd004-city-registry-001.clar" + +[contracts.test-ccd004-city-registry-002] +path = "tests/contracts/proposals/test-ccd004-city-registry-002.clar" + +[contracts.test-ccd005-city-data-001] +path = "tests/contracts/proposals/test-ccd005-city-data-001.clar" + +[contracts.test-ccd005-city-data-002] +path = "tests/contracts/proposals/test-ccd005-city-data-002.clar" + +[contracts.test-ccd005-city-data-003] +path = "tests/contracts/proposals/test-ccd005-city-data-003.clar" + +[contracts.test-ccd005-city-data-004] +path = "tests/contracts/proposals/test-ccd005-city-data-004.clar" + +[contracts.test-ccd005-city-data-005] +path = "tests/contracts/proposals/test-ccd005-city-data-005.clar" + +[contracts.test-ccd005-city-data-006] +path = "tests/contracts/proposals/test-ccd005-city-data-006.clar" + +[contracts.test-ccd005-city-data-007] +path = "tests/contracts/proposals/test-ccd005-city-data-007.clar" + +[contracts.test-ccd005-city-data-008] +path = "tests/contracts/proposals/test-ccd005-city-data-008.clar" + +[contracts.test-ccd005-city-data-009] +path = "tests/contracts/proposals/test-ccd005-city-data-009.clar" + +[contracts.test-ccd005-city-data-010] +path = "tests/contracts/proposals/test-ccd005-city-data-010.clar" + +[contracts.test-ccd005-city-data-011] +path = "tests/contracts/proposals/test-ccd005-city-data-011.clar" + +[contracts.test-ccd005-city-data-012] +path = "tests/contracts/proposals/test-ccd005-city-data-012.clar" + +[contracts.test-ccd005-city-data-013] +path = "tests/contracts/proposals/test-ccd005-city-data-013.clar" + +[contracts.test-ccd005-city-data-014] +path = "tests/contracts/proposals/test-ccd005-city-data-014.clar" + +[contracts.test-ccd005-city-data-015] +path = "tests/contracts/proposals/test-ccd005-city-data-015.clar" + +[contracts.test-ccd005-city-data-016] +path = "tests/contracts/proposals/test-ccd005-city-data-016.clar" + +[contracts.test-ccd005-city-data-017] +path = "tests/contracts/proposals/test-ccd005-city-data-017.clar" + +[contracts.test-ccd005-city-data-018] +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" + +[contracts.test-ccd006-citycoin-mining-002] +path = "tests/contracts/proposals/test-ccd006-citycoin-mining-002.clar" + +[contracts.test-ccd006-citycoin-mining-003] +path = "tests/contracts/proposals/test-ccd006-citycoin-mining-003.clar" + +[contracts.test-ccd006-citycoin-mining-004] +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" + +[contracts.test-ccd007-citycoin-stacking-002] +path = "tests/contracts/proposals/test-ccd007-citycoin-stacking-002.clar" + +[contracts.test-ccd007-citycoin-stacking-003] +path = "tests/contracts/proposals/test-ccd007-citycoin-stacking-003.clar" + +[contracts.test-ccd007-citycoin-stacking-004] +path = "tests/contracts/proposals/test-ccd007-citycoin-stacking-004.clar" + +[contracts.test-ccd007-citycoin-stacking-005] +path = "tests/contracts/proposals/test-ccd007-citycoin-stacking-005.clar" + +[contracts.test-ccd007-citycoin-stacking-006] +path = "tests/contracts/proposals/test-ccd007-citycoin-stacking-006.clar" + +[contracts.test-ccd007-citycoin-stacking-007] +path = "tests/contracts/proposals/test-ccd007-citycoin-stacking-007.clar" + +[contracts.test-ccd007-citycoin-stacking-008] +path = "tests/contracts/proposals/test-ccd007-citycoin-stacking-008.clar" + +[contracts.test-ccd007-citycoin-stacking-009] +path = "tests/contracts/proposals/test-ccd007-citycoin-stacking-009.clar" + +[contracts.test-ccd007-citycoin-stacking-010] +path = "tests/contracts/proposals/test-ccd007-citycoin-stacking-010.clar" + +[contracts.test-ccd007-citycoin-stacking-011] +path = "tests/contracts/proposals/test-ccd007-citycoin-stacking-011.clar" + +[contracts.test-ccd007-citycoin-stacking-012] +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" + +[contracts.test-ccip020-shutdown-001] +path = "tests/contracts/proposals/test-ccip020-shutdown-001.clar" +clarity_version = 2 +epoch = 2.4 + +[repl] +costs_version = 2 +parser_version = 2 + +# TEMPORARILY DISABLED + +# [repl.analysis] +# passes = ["check_checker"] + +# [repl.analysis.check_checker] +# strict = false +# trusted_sender = false +# trusted_caller = false +# callee_filter = true diff --git a/Clarinet.toml b/Clarinet.toml index 58bd761..8b825a1 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -143,7 +143,7 @@ path = "contracts/extensions/ccd011-stacking-payouts.clar" [contracts.ccd012-redemption-nyc] path = "contracts/extensions/ccd012-redemption-nyc.clar" clarity_version = 2 -epoch = "2.5" +epoch = 2.5 [contracts.ccip012-bootstrap] path = "contracts/proposals/ccip012-bootstrap.clar" From c1df83b0c3c819a244b4ff5eaca635ce20dea181 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Mon, 20 May 2024 13:58:11 -0700 Subject: [PATCH 12/65] fix: update testing strategy If we change epoch from 2.5 to 2.4 there are no changes to Clarity and older test structure can still be used. This will allow codecov to capture the changes for the new contracts and unifies everything back under a single Clarinet.toml file. The GH action is updated to run clarinet check with both legacy and latest versions, as well as run the legacy tests with a clear note by each job name. --- .github/workflows/test-contracts.yaml | 6 +- Clarinet.legacy.toml | 538 ------------------ Clarinet.toml | 4 +- ...022-citycoins-treasury-redemption-nyc.clar | 2 +- 4 files changed, 8 insertions(+), 542 deletions(-) delete mode 100644 Clarinet.legacy.toml diff --git a/.github/workflows/test-contracts.yaml b/.github/workflows/test-contracts.yaml index abce20d..de67fbd 100644 --- a/.github/workflows/test-contracts.yaml +++ b/.github/workflows/test-contracts.yaml @@ -28,10 +28,14 @@ jobs: uses: docker://hirosystems/clarinet:2.6.0 with: args: check + - name: "Check contract syntax (legacy)" + uses: docker://hirosystems/clarinet:1.8.0 + with: + args: check - name: "Run all contract tests (legacy)" uses: docker://hirosystems/clarinet:1.8.0 with: - args: test --coverage --manifest-path Clarinet.legacy.toml + args: test --coverage - name: "Upload code coverage" uses: codecov/codecov-action@v4 with: diff --git a/Clarinet.legacy.toml b/Clarinet.legacy.toml deleted file mode 100644 index 56ef02f..0000000 --- a/Clarinet.legacy.toml +++ /dev/null @@ -1,538 +0,0 @@ -[project] -name = "citycoins-protocol" -authors = [] -description = "" -telemetry = false -boot_contracts = ["pox", "costs-v2", "bns"] - -[project.cache_location] -path = ".requirements" - -# MAINNET REQUIREMENTS - -[[project.requirements]] -contract_id = "SP000000000000000000002Q6VF78.pox" - -[[project.requirements]] -contract_id = "SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard" - -[[project.requirements]] -contract_id = "SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait" - -[[project.requirements]] -contract_id = "SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-core-v2-trait" - -[[project.requirements]] -contract_id = "SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-vrf-v2" - -[[project.requirements]] -contract_id = "SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-auth-v2" - -[[project.requirements]] -contract_id = "SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v2" - -[[project.requirements]] -contract_id = "SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2" - -[[project.requirements]] -contract_id = "SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-auth-v2" - -[[project.requirements]] -contract_id = "SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-core-v2" - -[[project.requirements]] -contract_id = "SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2" - -# TESTNET REQUIREMENTS - -[[project.requirements]] -contract_id = "ST000000000000000000002AMW42H.pox" - -[[project.requirements]] -contract_id = "ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.sip-010-trait-ft-standard" - -[[project.requirements]] -contract_id = "ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.nft-trait" - -[[project.requirements]] -contract_id = "ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-core-v2-trait" - -[[project.requirements]] -contract_id = "ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-vrf-v2" - -[[project.requirements]] -contract_id = "ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-auth-v2" - -[[project.requirements]] -contract_id = "ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-core-v2" - -[[project.requirements]] -contract_id = "ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-token-v2" - -[[project.requirements]] -contract_id = "STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-auth-v2" - -[[project.requirements]] -contract_id = "STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-core-v2" - -[[project.requirements]] -contract_id = "STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-token-v2" - -# CITYCOINS PROTOCOL CONTRACTS - -[contracts.base-dao] -path = "contracts/base-dao.clar" - -[contracts.ccd001-direct-execute] -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-mining-v3] -# path = "contracts/extensions/ccd002-treasury-v3.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-mining-v3] -# path = "contracts/extensions/ccd002-treasury-v3.clar" - -[contracts.ccd002-treasury-nyc-stacking] -path = "contracts/extensions/ccd002-treasury.clar" - -[contracts.ccd003-user-registry] -path = "contracts/extensions/ccd003-user-registry.clar" - -[contracts.ccd004-city-registry] -path = "contracts/extensions/ccd004-city-registry.clar" - -[contracts.ccd005-city-data] -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" - -[contracts.ccd008-city-activation] -path = "contracts/extensions/ccd008-city-activation.clar" - -[contracts.ccd009-auth-v2-adapter] -path = "contracts/extensions/ccd009-auth-v2-adapter.clar" - -[contracts.ccd010-core-v2-adapter] -path = "contracts/extensions/ccd010-core-v2-adapter.clar" - -[contracts.ccd011-stacking-payouts] -path = "contracts/extensions/ccd011-stacking-payouts.clar" - -[contracts.ccip012-bootstrap] -path = "contracts/proposals/ccip012-bootstrap.clar" - -[contracts.ccip013-migration] -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 - -[contracts.ccip017-extend-sunset-period] -path = "contracts/proposals/ccip017-extend-sunset-period.clar" -clarity_version = 2 -epoch = 2.4 - -[contracts.ccip020-graceful-protocol-shutdown] -path = "contracts/proposals/ccip020-graceful-protocol-shutdown.clar" -clarity_version = 2 -epoch = 2.4 - -[contracts.ccip021-extend-sunset-period-2] -path = "contracts/proposals/ccip021-extend-sunset-period-2.clar" -clarity_version = 2 -epoch = 2.4 - -# CITYCOINS PROTOCOL TRAITS - -[contracts.extension-trait] -path = "contracts/traits/extension-trait.clar" - -[contracts.proposal-trait] -path = "contracts/traits/proposal-trait.clar" - -[contracts.stacking-trait] -path = "contracts/traits/stacking-trait.clar" - -[contracts.ccd002-trait] -path = "contracts/traits/ccd002-trait.clar" - -[contracts.ccd006-trait] -path = "contracts/traits/ccd006-trait.clar" - -[contracts.ccd007-trait] -path = "contracts/traits/ccd007-trait.clar" - -[contracts.ccip015-trait] -path = "contracts/traits/ccip015-trait.clar" - -# CITYCOINS EXTERNAL CONTRACTS - -[contracts.citycoin-vrf-v2] -path = "tests/contracts/external/citycoin-vrf-v2.clar" - -[contracts.test-ccext-governance-token-mia] -path = "tests/contracts/external/test-ccext-governance-token-mia.clar" - -[contracts.test-ccext-governance-token-nyc] -path = "tests/contracts/external/test-ccext-governance-token-nyc.clar" - -[contracts.test-ccext-nft-mia] -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-vrf] -path = "contracts/legacy/citycoin-vrf.clar" - -[contracts.citycoin-core-trait] -path = "contracts/legacy/citycoin-core-trait.clar" - -[contracts.citycoin-core-v2-trait] -path = "contracts/legacy/citycoin-core-v2-trait.clar" - -[contracts.citycoin-token-trait] -path = "contracts/legacy/citycoin-token-trait.clar" - -[contracts.citycoin-token-v2-trait] -path = "contracts/legacy/citycoin-token-v2-trait.clar" - -[contracts.miamicoin-auth] -path = "contracts/legacy/miamicoin-auth.clar" - -[contracts.miamicoin-core-v1] -path = "contracts/legacy/miamicoin-core-v1.clar" - -[contracts.miamicoin-token] -path = "contracts/legacy/miamicoin-token.clar" - -[contracts.miamicoin-core-v1-patch] -path = "contracts/legacy/miamicoin-core-v1-patch.clar" - -[contracts.miamicoin-auth-v2] -path = "contracts/legacy/miamicoin-auth-v2.clar" - -[contracts.miamicoin-core-v2] -path = "contracts/legacy/miamicoin-core-v2.clar" - -[contracts.miamicoin-token-v2] -path = "contracts/legacy/miamicoin-token-v2.clar" - -[contracts.newyorkcitycoin-auth] -path = "contracts/legacy/newyorkcitycoin-auth.clar" - -[contracts.newyorkcitycoin-core-v1] -path = "contracts/legacy/newyorkcitycoin-core-v1.clar" - -[contracts.newyorkcitycoin-token] -path = "contracts/legacy/newyorkcitycoin-token.clar" - -[contracts.newyorkcitycoin-core-v1-patch] -path = "contracts/legacy/newyorkcitycoin-core-v1-patch.clar" - -[contracts.newyorkcitycoin-auth-v2] -path = "contracts/legacy/newyorkcitycoin-auth-v2.clar" - -[contracts.newyorkcitycoin-core-v2] -path = "contracts/legacy/newyorkcitycoin-core-v2.clar" - -[contracts.newyorkcitycoin-token-v2] -path = "contracts/legacy/newyorkcitycoin-token-v2.clar" - -# CITYCOINS TEST PROPOSALS - -[contracts.test-ccd001-direct-execute-001] -path = "tests/contracts/proposals/test-ccd001-direct-execute-001.clar" - -[contracts.test-ccd001-direct-execute-002] -path = "tests/contracts/proposals/test-ccd001-direct-execute-002.clar" - -[contracts.test-ccd001-direct-execute-003] -path = "tests/contracts/proposals/test-ccd001-direct-execute-003.clar" - -[contracts.test-ccd002-treasury-001] -path = "tests/contracts/proposals/test-ccd002-treasury-001.clar" - -[contracts.test-ccd002-treasury-002] -path = "tests/contracts/proposals/test-ccd002-treasury-002.clar" - -[contracts.test-ccd002-treasury-003] -path = "tests/contracts/proposals/test-ccd002-treasury-003.clar" - -[contracts.test-ccd002-treasury-004] -path = "tests/contracts/proposals/test-ccd002-treasury-004.clar" - -[contracts.test-ccd002-treasury-005] -path = "tests/contracts/proposals/test-ccd002-treasury-005.clar" - -[contracts.test-ccd002-treasury-006] -path = "tests/contracts/proposals/test-ccd002-treasury-006.clar" - -[contracts.test-ccd002-treasury-007] -path = "tests/contracts/proposals/test-ccd002-treasury-007.clar" - -[contracts.test-ccd002-treasury-008] -path = "tests/contracts/proposals/test-ccd002-treasury-008.clar" - -[contracts.test-ccd002-treasury-009] -path = "tests/contracts/proposals/test-ccd002-treasury-009.clar" - -[contracts.test-ccd002-treasury-010] -path = "tests/contracts/proposals/test-ccd002-treasury-010.clar" - -[contracts.test-ccd002-treasury-011] -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" - -[contracts.test-ccd003-user-registry-002] -path = "tests/contracts/proposals/test-ccd003-user-registry-002.clar" - -[contracts.test-ccd003-user-registry-003] -path = "tests/contracts/proposals/test-ccd003-user-registry-003.clar" - -[contracts.test-ccd004-city-registry-001] -path = "tests/contracts/proposals/test-ccd004-city-registry-001.clar" - -[contracts.test-ccd004-city-registry-002] -path = "tests/contracts/proposals/test-ccd004-city-registry-002.clar" - -[contracts.test-ccd005-city-data-001] -path = "tests/contracts/proposals/test-ccd005-city-data-001.clar" - -[contracts.test-ccd005-city-data-002] -path = "tests/contracts/proposals/test-ccd005-city-data-002.clar" - -[contracts.test-ccd005-city-data-003] -path = "tests/contracts/proposals/test-ccd005-city-data-003.clar" - -[contracts.test-ccd005-city-data-004] -path = "tests/contracts/proposals/test-ccd005-city-data-004.clar" - -[contracts.test-ccd005-city-data-005] -path = "tests/contracts/proposals/test-ccd005-city-data-005.clar" - -[contracts.test-ccd005-city-data-006] -path = "tests/contracts/proposals/test-ccd005-city-data-006.clar" - -[contracts.test-ccd005-city-data-007] -path = "tests/contracts/proposals/test-ccd005-city-data-007.clar" - -[contracts.test-ccd005-city-data-008] -path = "tests/contracts/proposals/test-ccd005-city-data-008.clar" - -[contracts.test-ccd005-city-data-009] -path = "tests/contracts/proposals/test-ccd005-city-data-009.clar" - -[contracts.test-ccd005-city-data-010] -path = "tests/contracts/proposals/test-ccd005-city-data-010.clar" - -[contracts.test-ccd005-city-data-011] -path = "tests/contracts/proposals/test-ccd005-city-data-011.clar" - -[contracts.test-ccd005-city-data-012] -path = "tests/contracts/proposals/test-ccd005-city-data-012.clar" - -[contracts.test-ccd005-city-data-013] -path = "tests/contracts/proposals/test-ccd005-city-data-013.clar" - -[contracts.test-ccd005-city-data-014] -path = "tests/contracts/proposals/test-ccd005-city-data-014.clar" - -[contracts.test-ccd005-city-data-015] -path = "tests/contracts/proposals/test-ccd005-city-data-015.clar" - -[contracts.test-ccd005-city-data-016] -path = "tests/contracts/proposals/test-ccd005-city-data-016.clar" - -[contracts.test-ccd005-city-data-017] -path = "tests/contracts/proposals/test-ccd005-city-data-017.clar" - -[contracts.test-ccd005-city-data-018] -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" - -[contracts.test-ccd006-citycoin-mining-002] -path = "tests/contracts/proposals/test-ccd006-citycoin-mining-002.clar" - -[contracts.test-ccd006-citycoin-mining-003] -path = "tests/contracts/proposals/test-ccd006-citycoin-mining-003.clar" - -[contracts.test-ccd006-citycoin-mining-004] -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" - -[contracts.test-ccd007-citycoin-stacking-002] -path = "tests/contracts/proposals/test-ccd007-citycoin-stacking-002.clar" - -[contracts.test-ccd007-citycoin-stacking-003] -path = "tests/contracts/proposals/test-ccd007-citycoin-stacking-003.clar" - -[contracts.test-ccd007-citycoin-stacking-004] -path = "tests/contracts/proposals/test-ccd007-citycoin-stacking-004.clar" - -[contracts.test-ccd007-citycoin-stacking-005] -path = "tests/contracts/proposals/test-ccd007-citycoin-stacking-005.clar" - -[contracts.test-ccd007-citycoin-stacking-006] -path = "tests/contracts/proposals/test-ccd007-citycoin-stacking-006.clar" - -[contracts.test-ccd007-citycoin-stacking-007] -path = "tests/contracts/proposals/test-ccd007-citycoin-stacking-007.clar" - -[contracts.test-ccd007-citycoin-stacking-008] -path = "tests/contracts/proposals/test-ccd007-citycoin-stacking-008.clar" - -[contracts.test-ccd007-citycoin-stacking-009] -path = "tests/contracts/proposals/test-ccd007-citycoin-stacking-009.clar" - -[contracts.test-ccd007-citycoin-stacking-010] -path = "tests/contracts/proposals/test-ccd007-citycoin-stacking-010.clar" - -[contracts.test-ccd007-citycoin-stacking-011] -path = "tests/contracts/proposals/test-ccd007-citycoin-stacking-011.clar" - -[contracts.test-ccd007-citycoin-stacking-012] -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" - -[contracts.test-ccip020-shutdown-001] -path = "tests/contracts/proposals/test-ccip020-shutdown-001.clar" -clarity_version = 2 -epoch = 2.4 - -[repl] -costs_version = 2 -parser_version = 2 - -# TEMPORARILY DISABLED - -# [repl.analysis] -# passes = ["check_checker"] - -# [repl.analysis.check_checker] -# strict = false -# trusted_sender = false -# trusted_caller = false -# callee_filter = true diff --git a/Clarinet.toml b/Clarinet.toml index 8b825a1..dd8ac6c 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -143,7 +143,7 @@ path = "contracts/extensions/ccd011-stacking-payouts.clar" [contracts.ccd012-redemption-nyc] path = "contracts/extensions/ccd012-redemption-nyc.clar" clarity_version = 2 -epoch = 2.5 +epoch = 2.4 [contracts.ccip012-bootstrap] path = "contracts/proposals/ccip012-bootstrap.clar" @@ -182,7 +182,7 @@ epoch = 2.4 [contracts.ccip022-citycoins-treasury-redemption-nyc] path = "contracts/proposals/ccip022-citycoins-treasury-redemption-nyc.clar" clarity_version = 2 -epoch = 2.5 +epoch = 2.4 # CITYCOINS PROTOCOL TRAITS diff --git a/contracts/proposals/ccip022-citycoins-treasury-redemption-nyc.clar b/contracts/proposals/ccip022-citycoins-treasury-redemption-nyc.clar index 5ed08fa..98ebe47 100644 --- a/contracts/proposals/ccip022-citycoins-treasury-redemption-nyc.clar +++ b/contracts/proposals/ccip022-citycoins-treasury-redemption-nyc.clar @@ -26,7 +26,7 @@ (define-constant VOTE_SCALE_FACTOR (pow u10 u16)) ;; 16 decimal places ;; set city ID -(define-constant NYC_ID u2) ;; (unwrap! (contract-call? .ccd004-city-registry get-city-id "nyc") ERR_PANIC) +(define-constant NYC_ID (default-to u2 (contract-call? .ccd004-city-registry get-city-id "nyc"))) ;; DATA VARS From 8a1f7c19c8461c43691901716d2743473c2ad5aa Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Mon, 20 May 2024 15:47:07 -0700 Subject: [PATCH 13/65] fix: add model and tests for ccip022 --- Clarinet.toml | 9 +- .../extensions/ccd012-redemption-nyc.clar | 7 +- ...r => ccip022-treasury-redemption-nyc.clar} | 2 +- .../extensions/ccd012-redemption-nyc.model.ts | 0 .../ccip022-treasury-redemption-nyc.model.ts | 76 ++ .../test-ccd006-citycoin-mining-v2-002.clar | 1 + ...t-ccip022-treasury-redemption-nyc-001.clar | 63 ++ .../ccip022-treasury-redemption-nyc.test.ts | 660 ++++++++++++++++++ utils/common.ts | 2 + 9 files changed, 814 insertions(+), 6 deletions(-) rename contracts/proposals/{ccip022-citycoins-treasury-redemption-nyc.clar => ccip022-treasury-redemption-nyc.clar} (99%) create mode 100644 models/extensions/ccd012-redemption-nyc.model.ts create mode 100644 models/proposals/ccip022-treasury-redemption-nyc.model.ts create mode 100644 tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-001.clar create mode 100644 tests/proposals/ccip022-treasury-redemption-nyc.test.ts diff --git a/Clarinet.toml b/Clarinet.toml index dd8ac6c..bd45279 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -179,8 +179,8 @@ path = "contracts/proposals/ccip021-extend-sunset-period-2.clar" clarity_version = 2 epoch = 2.4 -[contracts.ccip022-citycoins-treasury-redemption-nyc] -path = "contracts/proposals/ccip022-citycoins-treasury-redemption-nyc.clar" +[contracts.ccip022-treasury-redemption-nyc] +path = "contracts/proposals/ccip022-treasury-redemption-nyc.clar" clarity_version = 2 epoch = 2.4 @@ -532,6 +532,11 @@ path = "tests/contracts/proposals/test-ccip020-shutdown-001.clar" clarity_version = 2 epoch = 2.4 +[contracts.test-ccip022-treasury-redemption-nyc-001] +path = "tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-001.clar" +clarity_version = 2 +epoch = 2.4 + [repl] costs_version = 2 parser_version = 2 diff --git a/contracts/extensions/ccd012-redemption-nyc.clar b/contracts/extensions/ccd012-redemption-nyc.clar index 13347c7..a702de0 100644 --- a/contracts/extensions/ccd012-redemption-nyc.clar +++ b/contracts/extensions/ccd012-redemption-nyc.clar @@ -57,9 +57,10 @@ ( ;; MAINNET: SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token ;; MAINNET: SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 - (nycTotalSupplyV1 (unwrap! (contract-call? .newyorkcitycoin-token get-total-supply) ERR_PANIC)) - (nycTotalSupplyV2 (unwrap! (contract-call? .newyorkcitycoin-token-v2 get-total-supply) ERR_PANIC)) - (nycTotalSupply (+ (* nycTotalSupplyV1 MICRO_CITYCOINS) nycTotalSupplyV2)) + (nycTotalSupplyV1 (unwrap! (contract-call? .test-ccext-governance-token-nyc get-total-supply) ERR_PANIC)) + (nycTotalSupplyV2 (unwrap! (contract-call? .test-ccext-governance-token-nyc get-total-supply) ERR_PANIC)) + ;; MAINNET: (nycTotalSupply (+ (* nycTotalSupplyV1 MICRO_CITYCOINS) nycTotalSupplyV2)) + (nycTotalSupply (+ nycTotalSupplyV1 nycTotalSupplyV2)) (nycRedemptionBalance (as-contract (stx-get-balance tx-sender))) ) ;; check if sender is DAO or extension diff --git a/contracts/proposals/ccip022-citycoins-treasury-redemption-nyc.clar b/contracts/proposals/ccip022-treasury-redemption-nyc.clar similarity index 99% rename from contracts/proposals/ccip022-citycoins-treasury-redemption-nyc.clar rename to contracts/proposals/ccip022-treasury-redemption-nyc.clar index 98ebe47..6309294 100644 --- a/contracts/proposals/ccip022-citycoins-treasury-redemption-nyc.clar +++ b/contracts/proposals/ccip022-treasury-redemption-nyc.clar @@ -61,7 +61,7 @@ ;; PUBLIC FUNCTIONS (define-public (execute (sender principal)) - (let ((nycRedemptionBalance (stx-get-balance .ccd002-treasury-nyc-mining))) + (let ((nycRedemptionBalance (stx-get-balance .ccd002-treasury-nyc-mining-v2))) ;; check vote is complete/passed (try! (is-executable)) ;; update vote variables diff --git a/models/extensions/ccd012-redemption-nyc.model.ts b/models/extensions/ccd012-redemption-nyc.model.ts new file mode 100644 index 0000000..e69de29 diff --git a/models/proposals/ccip022-treasury-redemption-nyc.model.ts b/models/proposals/ccip022-treasury-redemption-nyc.model.ts new file mode 100644 index 0000000..39b394f --- /dev/null +++ b/models/proposals/ccip022-treasury-redemption-nyc.model.ts @@ -0,0 +1,76 @@ +import { Chain, Account, Tx, types, ReadOnlyFn } from "../../utils/deps.ts"; + +enum ErrCode { + ERR_PANIC = 2200, + ERR_SAVING_VOTE, + ERR_VOTED_ALREADY, + ERR_NOTHING_STACKED, + ERR_USER_NOT_FOUND, + ERR_PROPOSAL_NOT_ACTIVE, + ERR_PROPOSAL_STILL_ACTIVE, + ERR_VOTE_FAILED, +} + +export class CCIP022TreasuryRedemptionNYC { + name = "ccip022-treasury-redemption-nyc"; + 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"); + } + + getVoteTotalNyc() { + return this.callReadOnlyFn("get-vote-total-nyc"); + } + + getVoteTotalNycOrDefault() { + return this.callReadOnlyFn("get-vote-total-nyc-or-default"); + } + + getVoteTotals() { + return this.callReadOnlyFn("get-vote-totals"); + } + + getVoterInfo(userId: number) { + return this.callReadOnlyFn("get-voter-info", [types.uint(userId)]); + } + + getNycVote(userId: number, scaled: boolean) { + return this.callReadOnlyFn("get-nyc-vote", [types.uint(userId), types.bool(scaled)]); + } + + // read-only function helper + private callReadOnlyFn(method: string, args: Array = [], sender: Account = this.deployer): ReadOnlyFn { + const result = this.chain.callReadOnlyFn(this.name, method, args, sender?.address); + return result; + } +} diff --git a/tests/contracts/proposals/test-ccd006-citycoin-mining-v2-002.clar b/tests/contracts/proposals/test-ccd006-citycoin-mining-v2-002.clar index 7cd8afc..e1e0ec6 100644 --- a/tests/contracts/proposals/test-ccd006-citycoin-mining-v2-002.clar +++ b/tests/contracts/proposals/test-ccd006-citycoin-mining-v2-002.clar @@ -8,6 +8,7 @@ (define-public (execute (sender principal)) (begin (try! (contract-call? .ccd005-city-data add-treasury u1 .ccd002-treasury-mia-mining-v2 "mining-v2")) + (try! (contract-call? .ccd005-city-data add-treasury u2 .ccd002-treasury-nyc-mining-v2 "mining-v2")) (ok true) ) ) diff --git a/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-001.clar b/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-001.clar new file mode 100644 index 0000000..f68771f --- /dev/null +++ b/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-001.clar @@ -0,0 +1,63 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer +;; Description: +;; Sets up everything required for CCIP-022 + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (begin + ;; enable CityCoins V2 mining extension + (try! (contract-call? .base-dao set-extensions + (list + {extension: .ccd006-citycoin-mining-v2, enabled: true} + {extension: .ccd012-redemption-nyc, enabled: true} + ) + )) + ;; disable v1 mining, enable v2 + (try! (contract-call? .ccd006-citycoin-mining set-mining-enabled false)) + (try! (contract-call? .ccd006-citycoin-mining-v2 set-mining-enabled true)) + ;; 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-ccd006-citycoin-mining-v2-002 + nyc + (try! (contract-call? .ccd005-city-data add-treasury u1 .ccd002-treasury-mia-mining-v2 "mining-v2")) + (try! (contract-call? .ccd005-city-data add-treasury u2 .ccd002-treasury-nyc-mining-v2 "mining-v2")) + ;; 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-mia mint u1000 'ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC)) + (try! (contract-call? .test-ccext-governance-token-mia mint u1000 'ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND)) + (try! (contract-call? .test-ccext-governance-token-nyc mint u1000 'ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5)) + (try! (contract-call? .test-ccext-governance-token-nyc mint u1000 'ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG)) + (try! (contract-call? .test-ccext-governance-token-nyc mint u1000 'ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC)) + (try! (contract-call? .test-ccext-governance-token-nyc mint u1000 'ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND)) + ;; 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)) + ;; 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)) + ;; same operations for NYC + (try! (contract-call? .ccd005-city-data set-coinbase-amounts u2 u10 u100 u1000 u10000 u100000 u1000000 u10000000)) + (try! (contract-call? .ccd005-city-data set-coinbase-thresholds u2 u50 u60 u70 u80 u90)) + (try! (contract-call? .ccd005-city-data set-coinbase-details u2 u20 u1)) + (ok true) + ) +) diff --git a/tests/proposals/ccip022-treasury-redemption-nyc.test.ts b/tests/proposals/ccip022-treasury-redemption-nyc.test.ts new file mode 100644 index 0000000..78f9626 --- /dev/null +++ b/tests/proposals/ccip022-treasury-redemption-nyc.test.ts @@ -0,0 +1,660 @@ +import { Account, Clarinet, Chain, types } from "../../utils/deps.ts"; +import { constructAndPassProposal, passProposal, PROPOSALS, mia, nyc, CCD006_REWARD_DELAY } 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 { CCIP022TreasuryRedemptionNYC } from "../../models/proposals/ccip022-treasury-redemption-nyc.model.ts"; +import { CCIP014Pox3 } from "../../models/proposals/ccip014-pox-3.model.ts"; + +// helper function to print voting data for users 1, 2, and 3 +function printVotingData(ccd007: CCD007CityStacking, ccip022: CCIP022TreasuryRedemptionNYC) { + console.log("contract vote totals nyc:"); + console.log(JSON.stringify(ccip022.getVoteTotalNyc(), null, 2)); + console.log("contract vote totals:"); + console.log(JSON.stringify(ccip022.getVoteTotals(), null, 2)); + + console.log("user 1:"); + console.log(ccip022.getVoterInfo(1)); + console.log("user 1 NYC:"); + console.log(ccd007.getStacker(nyc.cityId, 2, 1)); + console.log(ccip022.getNycVote(1, false)); + console.log(ccip022.getNycVote(1, true)); + + console.log("user 2:"); + console.log(ccip022.getVoterInfo(2)); + console.log("user 2 NYC:"); + console.log(ccd007.getStacker(nyc.cityId, 2, 2)); + console.log(ccip022.getNycVote(2, false)); + console.log(ccip022.getNycVote(2, true)); + + console.log("user 3:"); + console.log(ccip022.getVoterInfo(3)); + console.log("user 3 NYC:"); + console.log(ccd007.getStacker(nyc.cityId, 2, 3)); + console.log(ccip022.getNycVote(3, false)); + console.log(ccip022.getNycVote(3, true)); +} + +Clarinet.test({ + name: "ccip-022: execute() fails with ERR_VOTE_FAILED if there are no votes", + fn(chain: Chain, accounts: Map) { + // arrange + + // initialize contracts + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + + // act + + // execute ccip-022 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_022); + + // assert + block.receipts[2].result.expectErr().expectUint(CCIP022TreasuryRedemptionNYC.ErrCode.ERR_VOTE_FAILED); + }, +}); + +Clarinet.test({ + name: "ccip-022: 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 ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(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); + + // initialize contracts + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod)]); + // make sure every transaction succeeded + for (let i = 0; i < stackingBlock.receipts.length; i++) { + stackingBlock.receipts[i].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([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, false), ccip022TreasuryRedemptionNyc.voteOnProposal(user2, false)]); + for (let i = 0; i < votingBlock.receipts.length; i++) { + votingBlock.receipts[i].result.expectOk().expectBool(true); + } + + /* double check voting data + console.log(`voting block:\n${JSON.stringify(votingBlock, null, 2)}`); + printVotingData(ccd007CityStacking, ccip022TreasuryRedemptionNyc); + */ + + // execute ccip-022 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_022); + + // assert + block.receipts[2].result.expectErr().expectUint(CCIP022TreasuryRedemptionNYC.ErrCode.ERR_VOTE_FAILED); + }, +}); + +Clarinet.test({ + name: "ccip-022: 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 ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(chain, sender); + + const blocksMined = 10; + const amountPerBlock = 25000000; + const amountStacked = 500; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + + // initialize contracts + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + + // mine to put some funds in the treasury + const miningEntries = Array.from({ length: blocksMined }, () => amountPerBlock); + const miningBlock = chain.mineBlock([ccd006CityMiningV2.mine(sender, nyc.cityName, miningEntries)]); + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([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); + + // blocks to claim is an array of block heights + // starting with the miningBlock height + // and ending after blocksMined blocks + const blocksToClaim = Array.from({ length: blocksMined }, (_, i) => miningBlock.height + i - 1); + + // claim mined blocks to increase total supply + const claimBlock = chain.mineBlock(blocksToClaim.map((height) => ccd006CityMiningV2.claimMiningReward(sender, nyc.cityName, height))); + for (let i = 0; i < claimBlock.receipts.length; i++) { + claimBlock.receipts[i].result.expectOk().expectBool(true); + } + + // act + + // execute two yes votes with MIA only + const votingBlock = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, true)]); + votingBlock.receipts[0].result.expectOk().expectBool(true); + + /* double check voting data + console.log(`voting block:\n${JSON.stringify(votingBlock, null, 2)}`); + printVotingData(ccd007CityStacking, ccip022TreasuryRedemptionNyc); + */ + + // execute ccip-022 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_022); + + // assert + block.receipts[2].result.expectOk().expectUint(3); + }, +}); + +Clarinet.test({ + name: "ccip-022: 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 user3 = accounts.get("wallet_3")!; + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(chain, sender); + + const blocksMined = 10; + const amountPerBlock = 25000000; + const amountStacked = 500; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + + // initialize contracts + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + + // mine to put some funds in the treasury + const miningEntries = Array.from({ length: blocksMined }, () => amountPerBlock); + const miningBlock = chain.mineBlock([ccd006CityMiningV2.mine(sender, nyc.cityName, miningEntries)]); + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked, lockPeriod)]); + // for length of mineBlock array, expectOk and expectBool(true) + for (let i = 0; i < stackingBlock.receipts.length; i++) { + stackingBlock.receipts[i].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); + + // blocks to claim is an array of block heights + // starting with the miningBlock height + // and ending after blocksMined blocks + const blocksToClaim = Array.from({ length: blocksMined }, (_, i) => miningBlock.height + i - 1); + + // claim mined blocks to increase total supply + const claimBlock = chain.mineBlock(blocksToClaim.map((height) => ccd006CityMiningV2.claimMiningReward(sender, nyc.cityName, height))); + for (let i = 0; i < claimBlock.receipts.length; i++) { + claimBlock.receipts[i].result.expectOk().expectBool(true); + } + + // act + + // execute two yes votes, one no vote + const votingBlock = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user2, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user3, false)]); + for (let i = 0; i < votingBlock.receipts.length; i++) { + votingBlock.receipts[i].result.expectOk().expectBool(true); + } + + // double check voting data + // console.log(`voting block:\n${JSON.stringify(votingBlock, null, 2)}`); + // printVotingData(ccd007CityStacking, ccip022TreasuryRedemptionNyc); + + // execute ccip-022 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_022); + + // assert + block.receipts[2].result.expectOk().expectUint(3); + }, +}); + +Clarinet.test({ + name: "ccip-022: 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 user3 = accounts.get("wallet_3")!; + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(chain, sender); + + const blocksMined = 10; + const amountPerBlock = 25000000; + const amountStacked = 500; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + + // initialize contracts + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + + // mine to put some funds in the treasury + const miningEntries = Array.from({ length: blocksMined }, () => amountPerBlock); + const miningBlock = chain.mineBlock([ccd006CityMiningV2.mine(sender, nyc.cityName, miningEntries)]); + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked, lockPeriod)]); + for (let i = 0; i < stackingBlock.receipts.length; i++) { + stackingBlock.receipts[i].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); + + // blocks to claim is an array of block heights + // starting with the miningBlock height + // and ending after blocksMined blocks + const blocksToClaim = Array.from({ length: blocksMined }, (_, i) => miningBlock.height + i - 1); + + // claim mined blocks to increase total supply + const claimBlock = chain.mineBlock(blocksToClaim.map((height) => ccd006CityMiningV2.claimMiningReward(sender, nyc.cityName, height))); + for (let i = 0; i < claimBlock.receipts.length; i++) { + claimBlock.receipts[i].result.expectOk().expectBool(true); + } + + // act + + // execute two yes votes, one no vote + const votingBlock = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user2, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user3, false)]); + for (let i = 0; i < votingBlock.receipts.length; i++) { + votingBlock.receipts[i].result.expectOk().expectBool(true); + } + + /* double check voting data + console.log("BEFORE REVERSAL"); + console.log(`voting block:\n${JSON.stringify(votingBlock, null, 2)}`); + printVotingData(ccd007CityStacking, ccip022TreasuryRedemptionNyc); + */ + + const votingBlockReversed = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user3, true)]); + votingBlockReversed.receipts[0].result.expectOk().expectBool(true); + + /* double check voting data + console.log("AFTER REVERSAL"); + console.log(`voting block reversed:\n${JSON.stringify(votingBlockReversed, null, 2)}`); + printVotingData(ccd007CityStacking, ccip022TreasuryRedemptionNyc); + */ + + // execute ccip-022 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_022); + + // assert + block.receipts[2].result.expectOk().expectUint(3); + }, +}); + +Clarinet.test({ + name: "ccip-022: 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 ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(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 (sets up cities, tokens, and data) + const constructBlock = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + constructBlock.receipts[0].result.expectOk().expectBool(true); + + // mine to put funds in the mining treasury + const miningBlock = chain.mineBlock([ccd006CityMiningV2.mine(user1, nyc.cityName, miningEntries), ccd006CityMiningV2.mine(user2, nyc.cityName, miningEntries)]); + for (let i = 0; i < miningBlock.receipts.length; i++) { + miningBlock.receipts[i].result.expectOk().expectBool(true); + } + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked / 2, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked / 2, lockPeriod)]); + for (let i = 0; i < stackingBlock.receipts.length; i++) { + stackingBlock.receipts[i].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 + const votingBlock = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user2, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user3, true)]); + + // assert + votingBlock.receipts[0].result.expectOk().expectBool(true); + votingBlock.receipts[1].result.expectOk().expectBool(true); + votingBlock.receipts[2].result.expectErr().expectUint(CCIP022TreasuryRedemptionNYC.ErrCode.ERR_USER_NOT_FOUND); + }, +}); + +Clarinet.test({ + name: "ccip-022: 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 user3 = accounts.get("wallet_3"); + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(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 (sets up cities, tokens, and data) + const constructBlock = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + constructBlock.receipts[0].result.expectOk().expectBool(true); + + // mine to put funds in the mining treasury + const miningBlock = chain.mineBlock([ccd006CityMiningV2.mine(user1, nyc.cityName, miningEntries), ccd006CityMiningV2.mine(user2, nyc.cityName, miningEntries)]); + for (let i = 0; i < miningBlock.receipts.length; i++) { + miningBlock.receipts[i].result.expectOk().expectBool(true); + } + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked, lockPeriod)]); + for (let i = 0; i < stackingBlock.receipts.length; i++) { + stackingBlock.receipts[i].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 and 2 vote yes + // user 3 votes no + const votingBlock = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user2, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user3, false)]); + for (let i = 0; i < votingBlock.receipts.length; i++) { + votingBlock.receipts[i].result.expectOk().expectBool(true); + } + + // execute ccip-022, ending the vote + passProposal(chain, accounts, PROPOSALS.CCIP_022); + + // act + // user 1 tries to reverse their vote + const votingBlockAfter = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, false)]); + + // assert + votingBlockAfter.receipts[0].result.expectErr().expectUint(CCIP022TreasuryRedemptionNYC.ErrCode.ERR_PROPOSAL_NOT_ACTIVE); + }, +}); + +Clarinet.test({ + name: "ccip-022: 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 ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(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 (sets up cities, tokens, and data) + const constructBlock = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + constructBlock.receipts[0].result.expectOk().expectBool(true); + + // mine to put funds in the mining treasury + const miningBlock = chain.mineBlock([ccd006CityMiningV2.mine(user1, nyc.cityName, miningEntries), ccd006CityMiningV2.mine(user2, nyc.cityName, miningEntries)]); + for (let i = 0; i < miningBlock.receipts.length; i++) { + miningBlock.receipts[i].result.expectOk().expectBool(true); + } + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked / 2, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked / 2, lockPeriod)]); + for (let i = 0; i < stackingBlock.receipts.length; i++) { + stackingBlock.receipts[i].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); + + // vote yes + const votingBlock = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, true)]); + votingBlock.receipts[0].result.expectOk().expectBool(true); + + // act + const votingBlockDupe = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, true)]); + + // assert + votingBlockDupe.receipts[0].result.expectErr().expectUint(CCIP022TreasuryRedemptionNYC.ErrCode.ERR_VOTED_ALREADY); + }, +}); + +Clarinet.test({ + name: "ccip-022: 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 user3 = accounts.get("wallet_3")!; + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(chain, sender); + + const blocksMined = 10; + const amountPerBlock = 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 (sets up cities, tokens, and data) + const constructBlock = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + constructBlock.receipts[0].result.expectOk().expectBool(true); + + // mine to put some funds in the treasury + const miningEntries = Array.from({ length: blocksMined }, () => amountPerBlock); + const miningBlock = chain.mineBlock([ccd006CityMiningV2.mine(user1, nyc.cityName, miningEntries)]); + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked, lockPeriod)]); + for (let i = 0; i < stackingBlock.receipts.length; i++) { + stackingBlock.receipts[i].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); + + // blocks to claim is an array of block heights + // starting with the miningBlock height + // and ending after blocksMined blocks + const blocksToClaim = Array.from({ length: blocksMined }, (_, i) => miningBlock.height + i - 1); + + // claim mined blocks to increase total supply + const claimBlock = chain.mineBlock(blocksToClaim.map((height) => ccd006CityMiningV2.claimMiningReward(user1, nyc.cityName, height))); + for (let i = 0; i < claimBlock.receipts.length; i++) { + claimBlock.receipts[i].result.expectOk().expectBool(true); + } + + // act + + // execute two yes votes, one no vote + const votingBlock = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user2, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user3, false)]); + for (let i = 0; i < votingBlock.receipts.length; i++) { + votingBlock.receipts[i].result.expectOk().expectBool(true); + } + + // double check voting data + // console.log("BEFORE REVERSAL"); + // console.log(`voting block:\n${JSON.stringify(votingBlock, null, 2)}`); + // printVotingData(ccd007CityStacking, ccip022TreasuryRedemptionNyc); + + // vote totals NYC + ccip022TreasuryRedemptionNyc + .getVoteTotalNyc() + .result.expectSome() + .expectTuple({ totalAmountNo: types.uint(0), totalAmountYes: types.uint(1500), totalVotesNo: types.uint(0), totalVotesYes: types.uint(3) }); + + // vote totals in contract (MIA+NYC+Totals) + ccip022TreasuryRedemptionNyc + .getVoteTotals() + .result.expectSome() + .expectTuple({ nyc: { totalAmountNo: types.uint(0), totalAmountYes: types.uint(1500), totalVotesNo: types.uint(0), totalVotesYes: types.uint(3) }, totals: { totalAmountNo: types.uint(0), totalAmountYes: types.uint(2835), totalVotesNo: types.uint(0), totalVotesYes: types.uint(6) } }); + + // user 1 stats + ccip022TreasuryRedemptionNyc + .getVoterInfo(1) + .result.expectSome() + .expectTuple({ nyc: types.uint(500), vote: types.bool(true) }); + ccd007CityStacking.getStacker(nyc.cityId, 2, 1).result.expectTuple({ claimable: types.uint(0), stacked: types.uint(500) }); + ccip022TreasuryRedemptionNyc.getNycVote(1, false).result.expectSome().expectUint(500); + + // user 2 stats + ccip022TreasuryRedemptionNyc + .getVoterInfo(2) + .result.expectSome() + .expectTuple({ nyc: types.uint(500), vote: types.bool(true) }); + ccd007CityStacking.getStacker(nyc.cityId, 2, 2).result.expectTuple({ claimable: types.uint(0), stacked: types.uint(500) }); + ccip022TreasuryRedemptionNyc.getNycVote(2, false).result.expectSome().expectUint(500); + + // user 3 stats + ccip022TreasuryRedemptionNyc + .getVoterInfo(3) + .result.expectSome() + .expectTuple({ nyc: types.uint(500), vote: types.bool(false) }); + ccd007CityStacking.getStacker(nyc.cityId, 2, 3).result.expectTuple({ claimable: types.uint(0), stacked: types.uint(500) }); + ccip022TreasuryRedemptionNyc.getNycVote(3, false).result.expectSome().expectUint(500); + + // reverse the vote for user 3 + const votingBlockReversed = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user3, true)]); + votingBlockReversed.receipts[0].result.expectOk().expectBool(true); + + // vote totals NYC + ccip022TreasuryRedemptionNyc + .getVoteTotalNyc() + .result.expectSome() + .expectTuple({ totalAmountNo: types.uint(0), totalAmountYes: types.uint(1500), totalVotesNo: types.uint(0), totalVotesYes: types.uint(3) }); + // vote totals in contract (MIA+NYC+Totals) + ccip022TreasuryRedemptionNyc + .getVoteTotals() + .result.expectSome() + .expectTuple({ nyc: { totalAmountNo: types.uint(0), totalAmountYes: types.uint(1500), totalVotesNo: types.uint(0), totalVotesYes: types.uint(3) }, totals: { totalAmountNo: types.uint(0), totalAmountYes: types.uint(2835), totalVotesNo: types.uint(0), totalVotesYes: types.uint(6) } }); + + // user 1 stats + ccip022TreasuryRedemptionNyc + .getVoterInfo(1) + .result.expectSome() + .expectTuple({ nyc: types.uint(500), vote: types.bool(true) }); + ccd007CityStacking.getStacker(nyc.cityId, 2, 1).result.expectTuple({ claimable: types.uint(0), stacked: types.uint(500) }); + ccip022TreasuryRedemptionNyc.getNycVote(1, false).result.expectSome().expectUint(500); + + // user 2 stats + ccip022TreasuryRedemptionNyc + .getVoterInfo(2) + .result.expectSome() + .expectTuple({ nyc: types.uint(500), vote: types.bool(true) }); + ccd007CityStacking.getStacker(nyc.cityId, 2, 2).result.expectTuple({ claimable: types.uint(0), stacked: types.uint(500) }); + ccip022TreasuryRedemptionNyc.getNycVote(2, false).result.expectSome().expectUint(500); + + // user 3 stats + ccip022TreasuryRedemptionNyc + .getVoterInfo(3) + .result.expectSome() + .expectTuple({ nyc: types.uint(500), vote: types.bool(true) }); + ccd007CityStacking.getStacker(nyc.cityId, 2, 3).result.expectTuple({ claimable: types.uint(0), stacked: types.uint(500) }); + ccip022TreasuryRedemptionNyc.getNycVote(3, false).result.expectSome().expectUint(500); + + // double check voting data + //console.log("AFTER REVERSAL"); + //console.log(`voting block reversed:\n${JSON.stringify(votingBlockReversed, null, 2)}`); + //printVotingData(ccd007CityStacking, ccip022TreasuryRedemptionNyc); + + // execute ccip-022 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_022); + + // assert + block.receipts[2].result.expectOk().expectUint(3); + }, +}); + +/* +Clarinet.test({ + name: "", + fn(chain: Chain, accounts: Map) { + // arrange + + // act + + // assert + } +}) +*/ diff --git a/utils/common.ts b/utils/common.ts index 09a8b68..2d355c8 100644 --- a/utils/common.ts +++ b/utils/common.ts @@ -42,6 +42,7 @@ export const PROPOSALS = { CCIP_017: ADDRESS.concat(".ccip017-extend-sunset-period"), CCIP_020: ADDRESS.concat(".ccip020-graceful-protocol-shutdown"), CCIP_021: ADDRESS.concat(".ccip021-extend-sunset-period-2"), + CCIP_022: ADDRESS.concat(".ccip022-treasury-redemption-nyc"), 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"), @@ -119,6 +120,7 @@ export const PROPOSALS = { TEST_CCIP014_POX3_001: ADDRESS.concat(".test-ccip014-pox-3-001"), TEST_CCIP014_POX3_002: ADDRESS.concat(".test-ccip014-pox-3-002"), TEST_CCIP020_GRACEFUL_PROTOCOL_SHUTDOWN_001: ADDRESS.concat(".test-ccip020-shutdown-001"), + TEST_CCIP022_TREASURY_REDEMPTION_NYC_001: ADDRESS.concat(".test-ccip022-treasury-redemption-nyc-001"), }; export const EXTERNAL = { From dd1dbb0145b8046bcfca8151f3428db7d46de2ce Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Tue, 21 May 2024 12:55:17 -0700 Subject: [PATCH 14/65] chore: code cleanup --- tests/proposals/ccip022-treasury-redemption-nyc.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/proposals/ccip022-treasury-redemption-nyc.test.ts b/tests/proposals/ccip022-treasury-redemption-nyc.test.ts index 78f9626..f524f60 100644 --- a/tests/proposals/ccip022-treasury-redemption-nyc.test.ts +++ b/tests/proposals/ccip022-treasury-redemption-nyc.test.ts @@ -1,9 +1,8 @@ import { Account, Clarinet, Chain, types } from "../../utils/deps.ts"; -import { constructAndPassProposal, passProposal, PROPOSALS, mia, nyc, CCD006_REWARD_DELAY } from "../../utils/common.ts"; +import { constructAndPassProposal, passProposal, PROPOSALS, nyc } 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 { CCIP022TreasuryRedemptionNYC } from "../../models/proposals/ccip022-treasury-redemption-nyc.model.ts"; -import { CCIP014Pox3 } from "../../models/proposals/ccip014-pox-3.model.ts"; // helper function to print voting data for users 1, 2, and 3 function printVotingData(ccd007: CCD007CityStacking, ccip022: CCIP022TreasuryRedemptionNYC) { From ab7be274a70616233ad72d7191936d75182bb3f6 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Tue, 21 May 2024 13:09:42 -0700 Subject: [PATCH 15/65] fix: print redemption info in initialize fn --- contracts/extensions/ccd012-redemption-nyc.clar | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/extensions/ccd012-redemption-nyc.clar b/contracts/extensions/ccd012-redemption-nyc.clar index a702de0..2465318 100644 --- a/contracts/extensions/ccd012-redemption-nyc.clar +++ b/contracts/extensions/ccd012-redemption-nyc.clar @@ -80,7 +80,9 @@ ;; calculate redemption ratio (var-set redemptionRatio (/ nycRedemptionBalance nycTotalSupply)) ;; set redemptionsEnabled to true, can only run once - (ok (var-set redemptionsEnabled true)) + (var-set redemptionsEnabled true) + ;; print redemption info + (ok (print (get-redemption-info))) ) ) From a430f4a3be4bb4784c29ffa18659e8bb663ff2d9 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Tue, 21 May 2024 13:09:54 -0700 Subject: [PATCH 16/65] fix: add test outline in comments --- tests/extensions/ccd012-redemption-nyc.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 tests/extensions/ccd012-redemption-nyc.test.ts diff --git a/tests/extensions/ccd012-redemption-nyc.test.ts b/tests/extensions/ccd012-redemption-nyc.test.ts new file mode 100644 index 0000000..e6aa9ba --- /dev/null +++ b/tests/extensions/ccd012-redemption-nyc.test.ts @@ -0,0 +1,15 @@ +// initialize-redemption() fails with ERR_UNAUTHORIZED when called directly +// initialize-redemption() fails with ERR_GETTING_TOTAL_SUPPLY if both supplies are 0 +// initialize-redemption() fails with ERR_GETTING_REDEMPTION_BALANCE if the redemption balance is 0 +// initialize-redemption() fails with ERR_ALREADY_ENABLED if called more than once +// initialize-redemption() succeeds and prints the redemption info + +// redeem-nyc() fails with ERR_NOTHING_TO_REDEEM if the redemption amount is none +// redeem-nyc() fails with ERR_NOT_ENABLED if the redemption is not initialized +// redeem-nyc() fails with ERR_ALREADY_CLAIMED if the redemption is already claimed +// redeem-nyc() fails with ERR_BALANCE_NOT_FOUND if v1 or v2 tokens are not found +// redeem-nyc() fails with ERR_NOTHING_TO_REDEEM if the redemption amount is 0 + +// redeem-nyc() succeeds with just v1 tokens +// redeem-nyc() succeeds with just v2 tokens +// redeem-nyc() succeeds with both v1 and v2 tokens From 891249fc9ed4b91d3159cd7f0f197c63c776b0fe Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Tue, 21 May 2024 13:18:25 -0700 Subject: [PATCH 17/65] fix: setup test model for ccd012 --- .../extensions/ccd012-redemption-nyc.clar | 2 + .../extensions/ccd012-redemption-nyc.model.ts | 97 +++++++++++++++++++ .../extensions/ccd012-redemption-nyc.test.ts | 7 +- 3 files changed, 105 insertions(+), 1 deletion(-) diff --git a/contracts/extensions/ccd012-redemption-nyc.clar b/contracts/extensions/ccd012-redemption-nyc.clar index 2465318..9399e30 100644 --- a/contracts/extensions/ccd012-redemption-nyc.clar +++ b/contracts/extensions/ccd012-redemption-nyc.clar @@ -157,6 +157,7 @@ } ) +;; TODO: use provided address? (define-read-only (get-nyc-balances) (let ( @@ -186,6 +187,7 @@ ) ;; aggregate all exposed vars above +;; TODO: use provided address? (define-read-only (get-user-redemption-info) (let ( diff --git a/models/extensions/ccd012-redemption-nyc.model.ts b/models/extensions/ccd012-redemption-nyc.model.ts index e69de29..530206a 100644 --- a/models/extensions/ccd012-redemption-nyc.model.ts +++ b/models/extensions/ccd012-redemption-nyc.model.ts @@ -0,0 +1,97 @@ +import { Chain, Account, Tx, types, ReadOnlyFn } from "../../utils/deps.ts"; + +export enum ErrCode { + ERR_UNAUTHORIZED = 12000, + ERR_PANIC, + ERR_GETTING_TOTAL_SUPPLY, + ERR_GETTING_REDEMPTION_BALANCE, + ERR_ALREADY_ENABLED, + ERR_NOT_ENABLED, + ERR_BALANCE_NOT_FOUND, + ERR_NOTHING_TO_REDEEM, + ERR_ALREADY_CLAIMED, +} + +export class CCD012RedemptionNyc { + name: string; + static readonly ErrCode = ErrCode; + chain: Chain; + deployer: Account; + + constructor(chain: Chain, deployer: Account, name: string) { + this.name = name; + this.chain = chain; + this.deployer = deployer; + } + + // Authorization + + isDaoOrExtension(): ReadOnlyFn { + return this.callReadOnlyFn("is-dao-or-extension"); + } + + // Internal DAO functions + + initializeRedemption(sender: Account) { + return Tx.contractCall(this.name, "initialize-redemption", [], sender.address); + } + + redeemNyc(sender: Account) { + return Tx.contractCall(this.name, "redeem-nyc", [], sender.address); + } + + // Read only functions + + isRedemptionEnabled(): ReadOnlyFn { + return this.callReadOnlyFn("is-redemption-enabled", []); + } + + getRedemptionBlockHeight(): ReadOnlyFn { + return this.callReadOnlyFn("get-redemption-block-height", []); + } + + getRedemptionTotalSupply(): ReadOnlyFn { + return this.callReadOnlyFn("get-redemption-total-supply", []); + } + + getRedemptionContractBalance(): ReadOnlyFn { + return this.callReadOnlyFn("get-redemption-contract-balance", []); + } + + getRedemptionRatio(): ReadOnlyFn { + return this.callReadOnlyFn("get-redemption-ratio", []); + } + + getRedemptionInfo(): ReadOnlyFn { + return this.callReadOnlyFn("get-redemption-info", []); + } + + getNycBalances(): ReadOnlyFn { + return this.callReadOnlyFn("get-nyc-balances", []); + } + + getRedemptionForBalance(balance: number): ReadOnlyFn { + return this.callReadOnlyFn("get-redemption-for-balance", [types.uint(balance)]); + } + + getRedemptionAmountClaimed(address: string): ReadOnlyFn { + return this.callReadOnlyFn("get-redemption-amount-claimed", [types.principal(address)]); + } + + getUserRedemptionInfo(): ReadOnlyFn { + return this.callReadOnlyFn("get-user-redemption-info", []); + } + + // Extension callback + + callback(sender: Account, memo: string) { + return Tx.contractCall(this.name, "callback", [types.principal(sender.address), types.buff(memo)], sender.address); + } + + // Utility 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/tests/extensions/ccd012-redemption-nyc.test.ts b/tests/extensions/ccd012-redemption-nyc.test.ts index e6aa9ba..0b13f88 100644 --- a/tests/extensions/ccd012-redemption-nyc.test.ts +++ b/tests/extensions/ccd012-redemption-nyc.test.ts @@ -1,3 +1,9 @@ +import { Account, assertEquals, Clarinet, Chain } from "../../utils/deps.ts"; + +// ============================= +// TEST PLAN +// ============================= + // initialize-redemption() fails with ERR_UNAUTHORIZED when called directly // initialize-redemption() fails with ERR_GETTING_TOTAL_SUPPLY if both supplies are 0 // initialize-redemption() fails with ERR_GETTING_REDEMPTION_BALANCE if the redemption balance is 0 @@ -9,7 +15,6 @@ // redeem-nyc() fails with ERR_ALREADY_CLAIMED if the redemption is already claimed // redeem-nyc() fails with ERR_BALANCE_NOT_FOUND if v1 or v2 tokens are not found // redeem-nyc() fails with ERR_NOTHING_TO_REDEEM if the redemption amount is 0 - // redeem-nyc() succeeds with just v1 tokens // redeem-nyc() succeeds with just v2 tokens // redeem-nyc() succeeds with both v1 and v2 tokens From 0aab049a1ed5781adb7af7a7d57d67dd9188a4bd Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Tue, 21 May 2024 13:21:19 -0700 Subject: [PATCH 18/65] fix: add first two tests, simplify model name --- .../extensions/ccd012-redemption-nyc.model.ts | 4 +-- .../extensions/ccd012-redemption-nyc.test.ts | 35 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/models/extensions/ccd012-redemption-nyc.model.ts b/models/extensions/ccd012-redemption-nyc.model.ts index 530206a..20036bb 100644 --- a/models/extensions/ccd012-redemption-nyc.model.ts +++ b/models/extensions/ccd012-redemption-nyc.model.ts @@ -18,8 +18,8 @@ export class CCD012RedemptionNyc { chain: Chain; deployer: Account; - constructor(chain: Chain, deployer: Account, name: string) { - this.name = name; + constructor(chain: Chain, deployer: Account) { + this.name = "ccd012-redemption-nyc"; this.chain = chain; this.deployer = deployer; } diff --git a/tests/extensions/ccd012-redemption-nyc.test.ts b/tests/extensions/ccd012-redemption-nyc.test.ts index 0b13f88..fa18754 100644 --- a/tests/extensions/ccd012-redemption-nyc.test.ts +++ b/tests/extensions/ccd012-redemption-nyc.test.ts @@ -1,3 +1,4 @@ +import { CCD012RedemptionNyc } from "../../models/extensions/ccd012-redemption-nyc.model.ts"; import { Account, assertEquals, Clarinet, Chain } from "../../utils/deps.ts"; // ============================= @@ -18,3 +19,37 @@ import { Account, assertEquals, Clarinet, Chain } from "../../utils/deps.ts"; // redeem-nyc() succeeds with just v1 tokens // redeem-nyc() succeeds with just v2 tokens // redeem-nyc() succeeds with both v1 and v2 tokens + +// ============================= +// 0. AUTHORIZATION CHECKS +// ============================= + +Clarinet.test({ + name: "ccd012-redemption-nyc: is-dao-or-extension() fails when called directly", + fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const ccd012RedemptionNyc = new CCD012RedemptionNyc(chain, sender); + + // act + + // assert + ccd012RedemptionNyc.isDaoOrExtension().result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_UNAUTHORIZED); + }, +}); + +Clarinet.test({ + name: "ccd012-redemption-nyc: callback() succeeds when called directly", + fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const ccd012RedemptionNyc = new CCD012RedemptionNyc(chain, sender); + + // act + const { receipts } = chain.mineBlock([ccd012RedemptionNyc.callback(sender, "test")]); + + // assert + assertEquals(receipts.length, 1); + receipts[0].result.expectOk().expectBool(true); + }, +}); From 6dc4a8677824a3f958bffcca3c64e99c12887bd6 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Tue, 21 May 2024 16:25:16 -0700 Subject: [PATCH 19/65] chore: code cleanup --- contracts/proposals/ccip022-treasury-redemption-nyc.clar | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/proposals/ccip022-treasury-redemption-nyc.clar b/contracts/proposals/ccip022-treasury-redemption-nyc.clar index 6309294..dcfdd4f 100644 --- a/contracts/proposals/ccip022-treasury-redemption-nyc.clar +++ b/contracts/proposals/ccip022-treasury-redemption-nyc.clar @@ -36,7 +36,6 @@ (define-data-var voteEnd uint u0) ;; start the vote when deployed -;; TODO: stacks-block-height (var-set voteStart block-height) ;; DATA MAPS @@ -65,7 +64,6 @@ ;; check vote is complete/passed (try! (is-executable)) ;; update vote variables - ;; TODO: stacks-block-height (var-set voteEnd block-height) (var-set voteActive false) ;; transfer funds to new redemption extension From 7e3c7175d37acd44efbfa34dbbc55b7055341fb7 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Tue, 21 May 2024 16:45:07 -0700 Subject: [PATCH 20/65] chore: code cleanup, test prep Might need a 2nd test token contract to accurately simulate the balance lookup for both without modifying the contract logic. Also might be able to just use mia/nyc to represent two tokens / two balances too. --- .../extensions/ccd012-redemption-nyc.clar | 37 +++++++++---------- .../test-ccext-governance-token-nyc.clar | 9 +++++ 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/contracts/extensions/ccd012-redemption-nyc.clar b/contracts/extensions/ccd012-redemption-nyc.clar index 9399e30..ee3a605 100644 --- a/contracts/extensions/ccd012-redemption-nyc.clar +++ b/contracts/extensions/ccd012-redemption-nyc.clar @@ -72,7 +72,7 @@ ;; check if redemptions are already enabled (asserts! (not (var-get redemptionsEnabled)) ERR_ALREADY_ENABLED) ;; record current block height - (var-set blockHeight block-height) ;; TODO: stacks-block-height + (var-set blockHeight block-height) ;; record total supply at block height (var-set totalSupply nycTotalSupply) ;; record contract balance at block height @@ -92,8 +92,8 @@ (userAddress tx-sender) ;; MAINNET: SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token ;; MAINNET: SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 - (balanceV1 (unwrap! (contract-call? .newyorkcitycoin-token get-balance userAddress) ERR_BALANCE_NOT_FOUND)) - (balanceV2 (unwrap! (contract-call? .newyorkcitycoin-token-v2 get-balance userAddress) ERR_BALANCE_NOT_FOUND)) + (balanceV1 (unwrap! (contract-call? .test-ccext-governance-token-nyc get-balance userAddress) ERR_BALANCE_NOT_FOUND)) + (balanceV2 (unwrap! (contract-call? .test-ccext-governance-token-nyc get-balance userAddress) ERR_BALANCE_NOT_FOUND)) (totalBalance (+ (* balanceV1 MICRO_CITYCOINS) balanceV2)) (redemptionAmount (unwrap! (get-redemption-for-balance totalBalance) ERR_NOTHING_TO_REDEEM)) (redemptionClaims (default-to u0 (get-redemption-amount-claimed userAddress))) @@ -109,8 +109,8 @@ ;; burn NYC ;; MAINNET: SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token ;; MAINNET: SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 - (and (> u0 balanceV1) (try! (contract-call? .newyorkcitycoin-token burn balanceV1 userAddress))) - (and (> u0 balanceV2) (try! (contract-call? .newyorkcitycoin-token-v2 burn balanceV2 userAddress))) + (and (> u0 balanceV1) (try! (contract-call? .test-ccext-governance-token-nyc burn balanceV1 userAddress))) + (and (> u0 balanceV2) (try! (contract-call? .test-ccext-governance-token-nyc burn balanceV2 userAddress))) ;; transfer STX (try! (as-contract (stx-transfer? redemptionAmount tx-sender userAddress))) ;; update redemption claims @@ -118,7 +118,7 @@ ;; print redemption info (print (get-redemption-info)) ;; print user redemption info - (print (try! (get-user-redemption-info))) + (print (try! (get-user-redemption-info userAddress))) ;; return redemption amount (ok redemptionAmount) ) @@ -149,25 +149,25 @@ ;; aggregate all exposed vars above (define-read-only (get-redemption-info) { - redemptionsEnabled: (var-get redemptionsEnabled), - blockHeight: (var-get blockHeight), - totalSupply: (var-get totalSupply), - contractBalance: (var-get contractBalance), - redemptionRatio: (var-get redemptionRatio) + redemptionsEnabled: (is-redemption-enabled), + blockHeight: (get-redemption-block-height), + totalSupply: (get-redemption-total-supply), + contractBalance: (get-redemption-contract-balance), + redemptionRatio: (get-redemption-ratio) } ) -;; TODO: use provided address? -(define-read-only (get-nyc-balances) +(define-read-only (get-nyc-balances (address principal)) (let ( ;; MAINNET: SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token ;; MAINNET: SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 - (balanceV1 (unwrap! (contract-call? .newyorkcitycoin-token get-balance tx-sender) ERR_BALANCE_NOT_FOUND)) - (balanceV2 (unwrap! (contract-call? .newyorkcitycoin-token-v2 get-balance tx-sender) ERR_BALANCE_NOT_FOUND)) + (balanceV1 (unwrap! (contract-call? .test-ccext-governance-token-nyc get-balance address) ERR_BALANCE_NOT_FOUND)) + (balanceV2 (unwrap! (contract-call? .test-ccext-governance-token-nyc get-balance address) ERR_BALANCE_NOT_FOUND)) (totalBalance (+ (* balanceV1 MICRO_CITYCOINS) balanceV2)) ) (ok { + address: address, balanceV1: balanceV1, balanceV2: balanceV2, totalBalance: totalBalance @@ -187,16 +187,15 @@ ) ;; aggregate all exposed vars above -;; TODO: use provided address? -(define-read-only (get-user-redemption-info) +(define-read-only (get-user-redemption-info (address principal)) (let ( - (nycBalances (try! (get-nyc-balances))) + (nycBalances (try! (get-nyc-balances address))) (redemptionAmount (default-to u0 (get-redemption-for-balance (get totalBalance nycBalances)))) (redemptionClaims (default-to u0 (get-redemption-amount-claimed tx-sender))) ) (ok { - address: tx-sender, + address: address, nycBalances: nycBalances, redemptionAmount: redemptionAmount, redemptionClaims: redemptionClaims diff --git a/tests/contracts/external/test-ccext-governance-token-nyc.clar b/tests/contracts/external/test-ccext-governance-token-nyc.clar index b11757c..12085c3 100644 --- a/tests/contracts/external/test-ccext-governance-token-nyc.clar +++ b/tests/contracts/external/test-ccext-governance-token-nyc.clar @@ -38,10 +38,19 @@ )) ) +;; unguarded: simple mint function (define-public (mint (amount uint) (recipient principal)) (ft-mint? newyorkcitycoin amount recipient) ) +;; guarded: burn function (by user only) +(define-public (burn (amount uint) (owner principal)) + (begin + (asserts! (or (is-eq tx-sender owner) (is-eq contract-caller owner)) ERR_NOT_TOKEN_OWNER) + (ft-burn? newyorkcitycoin amount owner) + ) +) + ;; guarded: mint governance token (define-public (edg-mint (amount uint) (recipient principal)) (begin From 20d026278be3425fe222ab78be856b01edb52fbf Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Tue, 21 May 2024 16:46:41 -0700 Subject: [PATCH 21/65] fix: use address not tx-sender --- contracts/extensions/ccd012-redemption-nyc.clar | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/extensions/ccd012-redemption-nyc.clar b/contracts/extensions/ccd012-redemption-nyc.clar index ee3a605..96c89c1 100644 --- a/contracts/extensions/ccd012-redemption-nyc.clar +++ b/contracts/extensions/ccd012-redemption-nyc.clar @@ -192,7 +192,7 @@ ( (nycBalances (try! (get-nyc-balances address))) (redemptionAmount (default-to u0 (get-redemption-for-balance (get totalBalance nycBalances)))) - (redemptionClaims (default-to u0 (get-redemption-amount-claimed tx-sender))) + (redemptionClaims (default-to u0 (get-redemption-amount-claimed address))) ) (ok { address: address, From 8bad228d232c847e3455cd84a425924856612a47 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Tue, 21 May 2024 16:55:46 -0700 Subject: [PATCH 22/65] fix: update error numbers to 22XXX --- .../ccip022-treasury-redemption-nyc.clar | 16 ++++++++-------- .../ccip022-treasury-redemption-nyc.model.ts | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/contracts/proposals/ccip022-treasury-redemption-nyc.clar b/contracts/proposals/ccip022-treasury-redemption-nyc.clar index dcfdd4f..20abf98 100644 --- a/contracts/proposals/ccip022-treasury-redemption-nyc.clar +++ b/contracts/proposals/ccip022-treasury-redemption-nyc.clar @@ -5,14 +5,14 @@ ;; ERRORS -(define-constant ERR_PANIC (err u2200)) -(define-constant ERR_SAVING_VOTE (err u2201)) -(define-constant ERR_VOTED_ALREADY (err u2202)) -(define-constant ERR_NOTHING_STACKED (err u2203)) -(define-constant ERR_USER_NOT_FOUND (err u2204)) -(define-constant ERR_PROPOSAL_NOT_ACTIVE (err u2205)) -(define-constant ERR_PROPOSAL_STILL_ACTIVE (err u2206)) -(define-constant ERR_VOTE_FAILED (err u2207)) +(define-constant ERR_PANIC (err u22000)) +(define-constant ERR_SAVING_VOTE (err u22001)) +(define-constant ERR_VOTED_ALREADY (err u22002)) +(define-constant ERR_NOTHING_STACKED (err u22003)) +(define-constant ERR_USER_NOT_FOUND (err u22004)) +(define-constant ERR_PROPOSAL_NOT_ACTIVE (err u22005)) +(define-constant ERR_PROPOSAL_STILL_ACTIVE (err u22006)) +(define-constant ERR_VOTE_FAILED (err u22007)) ;; CONSTANTS diff --git a/models/proposals/ccip022-treasury-redemption-nyc.model.ts b/models/proposals/ccip022-treasury-redemption-nyc.model.ts index 39b394f..8ce5702 100644 --- a/models/proposals/ccip022-treasury-redemption-nyc.model.ts +++ b/models/proposals/ccip022-treasury-redemption-nyc.model.ts @@ -1,7 +1,7 @@ import { Chain, Account, Tx, types, ReadOnlyFn } from "../../utils/deps.ts"; enum ErrCode { - ERR_PANIC = 2200, + ERR_PANIC = 22000, ERR_SAVING_VOTE, ERR_VOTED_ALREADY, ERR_NOTHING_STACKED, From d55e92ac6f5860988efa045a3c3b78ef24a780a8 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Tue, 21 May 2024 17:03:07 -0700 Subject: [PATCH 23/65] fix: start adding tests --- .../extensions/ccd012-redemption-nyc.test.ts | 61 +++++++++++++------ 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/tests/extensions/ccd012-redemption-nyc.test.ts b/tests/extensions/ccd012-redemption-nyc.test.ts index fa18754..f759164 100644 --- a/tests/extensions/ccd012-redemption-nyc.test.ts +++ b/tests/extensions/ccd012-redemption-nyc.test.ts @@ -1,25 +1,10 @@ +import { CCD006CityMining } from "../../models/extensions/ccd006-citycoin-mining.model.ts"; +import { CCD007CityStacking } from "../../models/extensions/ccd007-citycoin-stacking.model.ts"; import { CCD012RedemptionNyc } from "../../models/extensions/ccd012-redemption-nyc.model.ts"; +import { CCIP022TreasuryRedemptionNYC } from "../../models/proposals/ccip022-treasury-redemption-nyc.model.ts"; +import { PROPOSALS, constructAndPassProposal, nyc, passProposal } from "../../utils/common.ts"; import { Account, assertEquals, Clarinet, Chain } from "../../utils/deps.ts"; -// ============================= -// TEST PLAN -// ============================= - -// initialize-redemption() fails with ERR_UNAUTHORIZED when called directly -// initialize-redemption() fails with ERR_GETTING_TOTAL_SUPPLY if both supplies are 0 -// initialize-redemption() fails with ERR_GETTING_REDEMPTION_BALANCE if the redemption balance is 0 -// initialize-redemption() fails with ERR_ALREADY_ENABLED if called more than once -// initialize-redemption() succeeds and prints the redemption info - -// redeem-nyc() fails with ERR_NOTHING_TO_REDEEM if the redemption amount is none -// redeem-nyc() fails with ERR_NOT_ENABLED if the redemption is not initialized -// redeem-nyc() fails with ERR_ALREADY_CLAIMED if the redemption is already claimed -// redeem-nyc() fails with ERR_BALANCE_NOT_FOUND if v1 or v2 tokens are not found -// redeem-nyc() fails with ERR_NOTHING_TO_REDEEM if the redemption amount is 0 -// redeem-nyc() succeeds with just v1 tokens -// redeem-nyc() succeeds with just v2 tokens -// redeem-nyc() succeeds with both v1 and v2 tokens - // ============================= // 0. AUTHORIZATION CHECKS // ============================= @@ -53,3 +38,41 @@ Clarinet.test({ receipts[0].result.expectOk().expectBool(true); }, }); + +// ============================= +// initialize-redemption() +// ============================= + +Clarinet.test({ + name: "ccd012-redemption-nyc: initialize-redemption() fails with ERR_UNAUTHORIZED when called directly", + fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const ccd012RedemptionNyc = new CCD012RedemptionNyc(chain, sender); + + // act + const initializeBlock = chain.mineBlock([ccd012RedemptionNyc.initializeRedemption(sender)]); + + // assert + assertEquals(initializeBlock.receipts.length, 1); + initializeBlock.receipts[0].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_UNAUTHORIZED); + }, +}); + +// initialize-redemption() fails with ERR_GETTING_TOTAL_SUPPLY if both supplies are 0 +// initialize-redemption() fails with ERR_GETTING_REDEMPTION_BALANCE if the redemption balance is 0 +// initialize-redemption() fails with ERR_ALREADY_ENABLED if called more than once +// initialize-redemption() succeeds and prints the redemption info + +// ============================= +// redeem-nyc() +// ============================= + +// redeem-nyc() fails with ERR_NOTHING_TO_REDEEM if the redemption amount is none +// redeem-nyc() fails with ERR_NOT_ENABLED if the redemption is not initialized +// redeem-nyc() fails with ERR_ALREADY_CLAIMED if the redemption is already claimed +// redeem-nyc() fails with ERR_BALANCE_NOT_FOUND if v1 or v2 tokens are not found +// redeem-nyc() fails with ERR_NOTHING_TO_REDEEM if the redemption amount is 0 +// redeem-nyc() succeeds with just v1 tokens +// redeem-nyc() succeeds with just v2 tokens +// redeem-nyc() succeeds with both v1 and v2 tokens From 6840d24ba77f0e82052288738a7343549e2749fd Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 22 May 2024 06:08:30 -0700 Subject: [PATCH 24/65] fix: use wallet besides deployer for cumulative test --- tests/extensions/ccd006-citycoin-mining.test.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/extensions/ccd006-citycoin-mining.test.ts b/tests/extensions/ccd006-citycoin-mining.test.ts index ed0b1fb..9dd4718 100644 --- a/tests/extensions/ccd006-citycoin-mining.test.ts +++ b/tests/extensions/ccd006-citycoin-mining.test.ts @@ -260,12 +260,14 @@ Clarinet.test({ fn(chain: Chain, accounts: Map) { // arrange const sender = accounts.get("deployer")!; + const wallet_2 = accounts.get("wallet_2")!; 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 // new balance is 99997999999992 after ccip-020 test deployment - const expectedBalance = 99997999999992; - const entries = [49998999999996, 49998999999996]; + // switched to another default account not deployer, normal balance + const expectedBalance = 100000000000000; + const entries = Array.from({ length: 2 }, () => expectedBalance / 2); // act @@ -273,7 +275,7 @@ Clarinet.test({ passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); - const block = chain.mineBlock([ccd006CityMining.mine(sender, miaCityName, entries)]); + const block = chain.mineBlock([ccd006CityMining.mine(wallet_2, miaCityName, entries)]); // assert ccd002Treasury.getBalanceStx().result.expectUint(expectedBalance); From 6457b0af8757086b2b458a79e93e325d293e44af Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 22 May 2024 06:40:45 -0700 Subject: [PATCH 25/65] fix: update test contracts to simulate more scenarios --- Clarinet.toml | 15 +++ ...t-ccip022-treasury-redemption-nyc-001.clar | 12 +- ...t-ccip022-treasury-redemption-nyc-002.clar | 18 +++ ...t-ccip022-treasury-redemption-nyc-003.clar | 19 +++ ...t-ccip022-treasury-redemption-nyc-004.clar | 14 +++ .../extensions/ccd012-redemption-nyc.test.ts | 116 +++++++++++++++++- .../ccip022-treasury-redemption-nyc.test.ts | 8 ++ utils/common.ts | 3 + 8 files changed, 193 insertions(+), 12 deletions(-) create mode 100644 tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-002.clar create mode 100644 tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-003.clar create mode 100644 tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-004.clar diff --git a/Clarinet.toml b/Clarinet.toml index bd45279..b8b2e53 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -537,6 +537,21 @@ path = "tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-001.clar" clarity_version = 2 epoch = 2.4 +[contracts.test-ccip022-treasury-redemption-nyc-002] +path = "tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-002.clar" +clarity_version = 2 +epoch = 2.4 + +[contracts.test-ccip022-treasury-redemption-nyc-003] +path = "tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-003.clar" +clarity_version = 2 +epoch = 2.4 + +[contracts.test-ccip022-treasury-redemption-nyc-004] +path = "tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-004.clar" +clarity_version = 2 +epoch = 2.4 + [repl] costs_version = 2 parser_version = 2 diff --git a/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-001.clar b/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-001.clar index f68771f..226f4ad 100644 --- a/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-001.clar +++ b/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-001.clar @@ -36,15 +36,6 @@ ;; 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-mia mint u1000 'ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC)) - (try! (contract-call? .test-ccext-governance-token-mia mint u1000 'ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND)) - (try! (contract-call? .test-ccext-governance-token-nyc mint u1000 'ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5)) - (try! (contract-call? .test-ccext-governance-token-nyc mint u1000 'ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG)) - (try! (contract-call? .test-ccext-governance-token-nyc mint u1000 'ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC)) - (try! (contract-call? .test-ccext-governance-token-nyc mint u1000 'ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND)) ;; 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)) @@ -61,3 +52,6 @@ (ok true) ) ) + +;; transfer 1M STX to mining treasury contract from deployer +(stx-transfer? u1000000000000 tx-sender .ccd002-treasury-nyc-mining-v2) diff --git a/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-002.clar b/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-002.clar new file mode 100644 index 0000000..1c9855b --- /dev/null +++ b/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-002.clar @@ -0,0 +1,18 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer +;; Description: +;; Prepares everything for CCD012 execution + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (begin + ;; to simulate newyorkcitycoin-token + (try! (contract-call? .test-ccext-governance-token-mia mint u1000000 'ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5)) + (try! (contract-call? .test-ccext-governance-token-mia mint u2000000 'ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG)) + (try! (contract-call? .test-ccext-governance-token-mia mint u3000000 'ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC)) + (try! (contract-call? .test-ccext-governance-token-mia mint u4000000 'ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND)) + (ok true) + ) +) diff --git a/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-003.clar b/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-003.clar new file mode 100644 index 0000000..e2308b3 --- /dev/null +++ b/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-003.clar @@ -0,0 +1,19 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer +;; Description: +;; Prepares everything for CCD012 execution + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (begin + ;; to simulate newyorkcitycoin-token-v2 + (try! (contract-call? .test-ccext-governance-token-nyc mint u1000000 'ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5)) + (try! (contract-call? .test-ccext-governance-token-nyc mint u2000000 'ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG)) + (try! (contract-call? .test-ccext-governance-token-nyc mint u3000000 'ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC)) + (try! (contract-call? .test-ccext-governance-token-nyc mint u4000000 'ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND)) + (ok true) + ) +) + diff --git a/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-004.clar b/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-004.clar new file mode 100644 index 0000000..a8b788b --- /dev/null +++ b/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-004.clar @@ -0,0 +1,14 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer +;; Description: +;; Tests calling initialize again + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (begin + (try! (contract-call? .ccd012-redemption-nyc initialize-redemption)) + (ok true) + ) +) diff --git a/tests/extensions/ccd012-redemption-nyc.test.ts b/tests/extensions/ccd012-redemption-nyc.test.ts index f759164..c995115 100644 --- a/tests/extensions/ccd012-redemption-nyc.test.ts +++ b/tests/extensions/ccd012-redemption-nyc.test.ts @@ -61,15 +61,125 @@ Clarinet.test({ // initialize-redemption() fails with ERR_GETTING_TOTAL_SUPPLY if both supplies are 0 // initialize-redemption() fails with ERR_GETTING_REDEMPTION_BALANCE if the redemption balance is 0 -// initialize-redemption() fails with ERR_ALREADY_ENABLED if called more than once + +Clarinet.test({ + name: "ccd012-redemption-nyc: initialize-redemption() fails with ERR_ALREADY_ENABLED if called more than once", + async 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 ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccd012RedemptionNyc = new CCD012RedemptionNyc(chain, sender); + const ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(chain, sender); + + // set stacking parameters + const amountStacked = 500; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + + // initialize contracts + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + + // mint and move funds + passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_003); + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked, lockPeriod)]); + // for length of mineBlock array, expectOk and expectBool(true) + for (let i = 0; i < stackingBlock.receipts.length; i++) { + stackingBlock.receipts[i].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 two yes votes, one no vote + const votingBlock = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user2, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user3, false)]); + for (let i = 0; i < votingBlock.receipts.length; i++) { + votingBlock.receipts[i].result.expectOk().expectBool(true); + } + + // execute ccip-022 + const executeBlock = passProposal(chain, accounts, PROPOSALS.CCIP_022); + executeBlock.receipts[0].result.expectOk().expectUint(1); + executeBlock.receipts[1].result.expectOk().expectUint(2); + executeBlock.receipts[2].result.expectOk().expectUint(3); + + // act + const initializeBlock = passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_004); + + // assert + assertEquals(initializeBlock.receipts.length, 3); + initializeBlock.receipts[0].result.expectOk().expectUint(1); + initializeBlock.receipts[1].result.expectOk().expectUint(2); + initializeBlock.receipts[2].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_ALREADY_ENABLED); + }, +}); + // initialize-redemption() succeeds and prints the redemption info // ============================= // redeem-nyc() // ============================= -// redeem-nyc() fails with ERR_NOTHING_TO_REDEEM if the redemption amount is none -// redeem-nyc() fails with ERR_NOT_ENABLED if the redemption is not initialized +Clarinet.test({ + name: "ccd012-redemption-nyc: redeem-nyc() fails with ERR_NOTHING_TO_REDEEM if the redemption amount is none", + async fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const ccd012RedemptionNyc = new CCD012RedemptionNyc(chain, sender); + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + + // initialize contracts + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + + // act + const redeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender)]); + + // assert + assertEquals(redeemBlock.receipts.length, 1); + redeemBlock.receipts[0].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_NOTHING_TO_REDEEM); + }, +}); + +Clarinet.test({ + name: "ccd012-redemption-nyc: redeem-nyc() fails with ERR_NOT_ENABLED if the redemption is not initialized", + async fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const ccd012RedemptionNyc = new CCD012RedemptionNyc(chain, sender); + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + + // initialize contracts + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_002); + + // act + const redeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender)]); + + // assert + assertEquals(redeemBlock.receipts.length, 1); + // note: actually fails with ERR_NOTHING_TO_REDEEM, but did not want to remove test case / extra code to be safe + // when disabled the redemption amount always returns none, so short-circuits early before error + redeemBlock.receipts[0].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_NOTHING_TO_REDEEM); + }, +}); + // redeem-nyc() fails with ERR_ALREADY_CLAIMED if the redemption is already claimed // redeem-nyc() fails with ERR_BALANCE_NOT_FOUND if v1 or v2 tokens are not found // redeem-nyc() fails with ERR_NOTHING_TO_REDEEM if the redemption amount is 0 diff --git a/tests/proposals/ccip022-treasury-redemption-nyc.test.ts b/tests/proposals/ccip022-treasury-redemption-nyc.test.ts index f524f60..0008852 100644 --- a/tests/proposals/ccip022-treasury-redemption-nyc.test.ts +++ b/tests/proposals/ccip022-treasury-redemption-nyc.test.ts @@ -70,6 +70,7 @@ Clarinet.test({ // initialize contracts constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_003); // stack first cycle u1, last cycle u10 const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod)]); @@ -126,6 +127,7 @@ Clarinet.test({ // initialize contracts constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_003); // mine to put some funds in the treasury const miningEntries = Array.from({ length: blocksMined }, () => amountPerBlock); @@ -194,6 +196,7 @@ Clarinet.test({ // initialize contracts constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_003); // mine to put some funds in the treasury const miningEntries = Array.from({ length: blocksMined }, () => amountPerBlock); @@ -266,6 +269,7 @@ Clarinet.test({ // initialize contracts constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_003); // mine to put some funds in the treasury const miningEntries = Array.from({ length: blocksMined }, () => amountPerBlock); @@ -348,6 +352,7 @@ Clarinet.test({ // prepare for CCIP (sets up cities, tokens, and data) const constructBlock = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); constructBlock.receipts[0].result.expectOk().expectBool(true); + passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_003); // mine to put funds in the mining treasury const miningBlock = chain.mineBlock([ccd006CityMiningV2.mine(user1, nyc.cityName, miningEntries), ccd006CityMiningV2.mine(user2, nyc.cityName, miningEntries)]); @@ -400,6 +405,7 @@ Clarinet.test({ // prepare for CCIP (sets up cities, tokens, and data) const constructBlock = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); constructBlock.receipts[0].result.expectOk().expectBool(true); + passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_003); // mine to put funds in the mining treasury const miningBlock = chain.mineBlock([ccd006CityMiningV2.mine(user1, nyc.cityName, miningEntries), ccd006CityMiningV2.mine(user2, nyc.cityName, miningEntries)]); @@ -461,6 +467,7 @@ Clarinet.test({ // prepare for CCIP (sets up cities, tokens, and data) const constructBlock = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); constructBlock.receipts[0].result.expectOk().expectBool(true); + passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_003); // mine to put funds in the mining treasury const miningBlock = chain.mineBlock([ccd006CityMiningV2.mine(user1, nyc.cityName, miningEntries), ccd006CityMiningV2.mine(user2, nyc.cityName, miningEntries)]); @@ -515,6 +522,7 @@ Clarinet.test({ // prepare for CCIP (sets up cities, tokens, and data) const constructBlock = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_003); constructBlock.receipts[0].result.expectOk().expectBool(true); // mine to put some funds in the treasury diff --git a/utils/common.ts b/utils/common.ts index 2d355c8..c519b6f 100644 --- a/utils/common.ts +++ b/utils/common.ts @@ -121,6 +121,9 @@ export const PROPOSALS = { TEST_CCIP014_POX3_002: ADDRESS.concat(".test-ccip014-pox-3-002"), TEST_CCIP020_GRACEFUL_PROTOCOL_SHUTDOWN_001: ADDRESS.concat(".test-ccip020-shutdown-001"), TEST_CCIP022_TREASURY_REDEMPTION_NYC_001: ADDRESS.concat(".test-ccip022-treasury-redemption-nyc-001"), + TEST_CCIP022_TREASURY_REDEMPTION_NYC_002: ADDRESS.concat(".test-ccip022-treasury-redemption-nyc-002"), + TEST_CCIP022_TREASURY_REDEMPTION_NYC_003: ADDRESS.concat(".test-ccip022-treasury-redemption-nyc-003"), + TEST_CCIP022_TREASURY_REDEMPTION_NYC_004: ADDRESS.concat(".test-ccip022-treasury-redemption-nyc-004"), }; export const EXTERNAL = { From a19fccc6edb2523bdc8914c3051b668669ad8d3d Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 22 May 2024 07:07:57 -0700 Subject: [PATCH 26/65] fix: add moar tests --- .../extensions/ccd012-redemption-nyc.test.ts | 31 +++++++++++-------- utils/common.ts | 1 + 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/tests/extensions/ccd012-redemption-nyc.test.ts b/tests/extensions/ccd012-redemption-nyc.test.ts index c995115..33f1625 100644 --- a/tests/extensions/ccd012-redemption-nyc.test.ts +++ b/tests/extensions/ccd012-redemption-nyc.test.ts @@ -2,8 +2,8 @@ import { CCD006CityMining } from "../../models/extensions/ccd006-citycoin-mining import { CCD007CityStacking } from "../../models/extensions/ccd007-citycoin-stacking.model.ts"; import { CCD012RedemptionNyc } from "../../models/extensions/ccd012-redemption-nyc.model.ts"; import { CCIP022TreasuryRedemptionNYC } from "../../models/proposals/ccip022-treasury-redemption-nyc.model.ts"; -import { PROPOSALS, constructAndPassProposal, nyc, passProposal } from "../../utils/common.ts"; -import { Account, assertEquals, Clarinet, Chain } from "../../utils/deps.ts"; +import { EXTENSIONS, PROPOSALS, constructAndPassProposal, nyc, passProposal } from "../../utils/common.ts"; +import { Account, assertEquals, Clarinet, Chain, types } from "../../utils/deps.ts"; // ============================= // 0. AUTHORIZATION CHECKS @@ -60,10 +60,20 @@ Clarinet.test({ }); // initialize-redemption() fails with ERR_GETTING_TOTAL_SUPPLY if both supplies are 0 +// note: supply is required for voting in CCIP-022, so unreachable + // initialize-redemption() fails with ERR_GETTING_REDEMPTION_BALANCE if the redemption balance is 0 +// note: transfer fails in CCIP-022 for this case, so unreachable Clarinet.test({ name: "ccd012-redemption-nyc: initialize-redemption() fails with ERR_ALREADY_ENABLED if called more than once", + async fn(chain: Chain, accounts: Map) { + 0; + }, +}); + +Clarinet.test({ + name: "ccd012-redemption-nyc: initialize-redemption() succeeds and prints the redemption info", async fn(chain: Chain, accounts: Map) { // arrange const sender = accounts.get("deployer")!; @@ -108,25 +118,20 @@ Clarinet.test({ votingBlock.receipts[i].result.expectOk().expectBool(true); } - // execute ccip-022 + // act const executeBlock = passProposal(chain, accounts, PROPOSALS.CCIP_022); + + // assert + assertEquals(executeBlock.receipts.length, 3); executeBlock.receipts[0].result.expectOk().expectUint(1); executeBlock.receipts[1].result.expectOk().expectUint(2); executeBlock.receipts[2].result.expectOk().expectUint(3); - // act - const initializeBlock = passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_004); - - // assert - assertEquals(initializeBlock.receipts.length, 3); - initializeBlock.receipts[0].result.expectOk().expectUint(1); - initializeBlock.receipts[1].result.expectOk().expectUint(2); - initializeBlock.receipts[2].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_ALREADY_ENABLED); + const expectedEvent = `{blockHeight: u${executeBlock.height - 1}, contractBalance: u1000000000000, redemptionRatio: u50000, redemptionsEnabled: true, totalSupply: u20000000}`; + executeBlock.receipts[2].events.expectPrintEvent(EXTENSIONS.CCD012_REDEMPTION_NYC, expectedEvent); }, }); -// initialize-redemption() succeeds and prints the redemption info - // ============================= // redeem-nyc() // ============================= diff --git a/utils/common.ts b/utils/common.ts index c519b6f..eaad8c3 100644 --- a/utils/common.ts +++ b/utils/common.ts @@ -32,6 +32,7 @@ export const EXTENSIONS = { CCD008_CITY_ACTIVATION: ADDRESS.concat(".ccd008-city-activation"), CCD009_AUTH_V2_ADAPTER: ADDRESS.concat(".ccd009-auth-v2-adapter"), CCD010_CORE_V2_ADAPTER: ADDRESS.concat(".ccd010-core-v2-adapter"), + CCD012_REDEMPTION_NYC: ADDRESS.concat(".ccd012-redemption-nyc"), }; export const PROPOSALS = { From 048064a5c6119107931365af964219db9024314f Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 22 May 2024 07:15:17 -0700 Subject: [PATCH 27/65] fix: cleanup failing test, skip ext --- tests/base-dao.test.ts | 1 + tests/extensions/ccd012-redemption-nyc.test.ts | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/base-dao.test.ts b/tests/base-dao.test.ts index d87fe12..4409e59 100644 --- a/tests/base-dao.test.ts +++ b/tests/base-dao.test.ts @@ -206,6 +206,7 @@ Clarinet.test({ 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 + if (ext === EXTENSIONS.CCD012_REDEMPTION_NYC) continue; // skip, not enabled until CCIP-022 //console.log("ext:", ext); //console.log("enabled:", baseDao.isExtension(ext).result); baseDao.isExtension(ext).result.expectBool(true); diff --git a/tests/extensions/ccd012-redemption-nyc.test.ts b/tests/extensions/ccd012-redemption-nyc.test.ts index 33f1625..6086c3b 100644 --- a/tests/extensions/ccd012-redemption-nyc.test.ts +++ b/tests/extensions/ccd012-redemption-nyc.test.ts @@ -1,4 +1,3 @@ -import { CCD006CityMining } from "../../models/extensions/ccd006-citycoin-mining.model.ts"; import { CCD007CityStacking } from "../../models/extensions/ccd007-citycoin-stacking.model.ts"; import { CCD012RedemptionNyc } from "../../models/extensions/ccd012-redemption-nyc.model.ts"; import { CCIP022TreasuryRedemptionNYC } from "../../models/proposals/ccip022-treasury-redemption-nyc.model.ts"; From a1866770cf21fe0566b3bb5ce9ad8679e5599669 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 22 May 2024 16:00:45 -0700 Subject: [PATCH 28/65] fix: starting to test the math Test contracts both have 6 decimals so MICRO_CITYCOINS multiplication removed from testing and marked with MAINNET comment lines. --- .../extensions/ccd012-redemption-nyc.clar | 14 +-- .../extensions/ccd012-redemption-nyc.model.ts | 8 +- .../test-ccext-governance-token-mia.clar | 9 ++ .../extensions/ccd012-redemption-nyc.test.ts | 96 +++++++++++++++++++ 4 files changed, 117 insertions(+), 10 deletions(-) diff --git a/contracts/extensions/ccd012-redemption-nyc.clar b/contracts/extensions/ccd012-redemption-nyc.clar index 96c89c1..eeb1cf4 100644 --- a/contracts/extensions/ccd012-redemption-nyc.clar +++ b/contracts/extensions/ccd012-redemption-nyc.clar @@ -57,7 +57,7 @@ ( ;; MAINNET: SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token ;; MAINNET: SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 - (nycTotalSupplyV1 (unwrap! (contract-call? .test-ccext-governance-token-nyc get-total-supply) ERR_PANIC)) + (nycTotalSupplyV1 (unwrap! (contract-call? .test-ccext-governance-token-mia get-total-supply) ERR_PANIC)) (nycTotalSupplyV2 (unwrap! (contract-call? .test-ccext-governance-token-nyc get-total-supply) ERR_PANIC)) ;; MAINNET: (nycTotalSupply (+ (* nycTotalSupplyV1 MICRO_CITYCOINS) nycTotalSupplyV2)) (nycTotalSupply (+ nycTotalSupplyV1 nycTotalSupplyV2)) @@ -92,9 +92,10 @@ (userAddress tx-sender) ;; MAINNET: SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token ;; MAINNET: SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 - (balanceV1 (unwrap! (contract-call? .test-ccext-governance-token-nyc get-balance userAddress) ERR_BALANCE_NOT_FOUND)) + (balanceV1 (unwrap! (contract-call? .test-ccext-governance-token-mia get-balance userAddress) ERR_BALANCE_NOT_FOUND)) (balanceV2 (unwrap! (contract-call? .test-ccext-governance-token-nyc get-balance userAddress) ERR_BALANCE_NOT_FOUND)) - (totalBalance (+ (* balanceV1 MICRO_CITYCOINS) balanceV2)) + ;; MAINNET: (totalBalance (+ (* balanceV1 MICRO_CITYCOINS) balanceV2)) + (totalBalance (+ balanceV1 balanceV2)) (redemptionAmount (unwrap! (get-redemption-for-balance totalBalance) ERR_NOTHING_TO_REDEEM)) (redemptionClaims (default-to u0 (get-redemption-amount-claimed userAddress))) ) @@ -109,7 +110,7 @@ ;; burn NYC ;; MAINNET: SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token ;; MAINNET: SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 - (and (> u0 balanceV1) (try! (contract-call? .test-ccext-governance-token-nyc burn balanceV1 userAddress))) + (and (> u0 balanceV1) (try! (contract-call? .test-ccext-governance-token-mia burn balanceV1 userAddress))) (and (> u0 balanceV2) (try! (contract-call? .test-ccext-governance-token-nyc burn balanceV2 userAddress))) ;; transfer STX (try! (as-contract (stx-transfer? redemptionAmount tx-sender userAddress))) @@ -162,9 +163,10 @@ ( ;; MAINNET: SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token ;; MAINNET: SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 - (balanceV1 (unwrap! (contract-call? .test-ccext-governance-token-nyc get-balance address) ERR_BALANCE_NOT_FOUND)) + (balanceV1 (unwrap! (contract-call? .test-ccext-governance-token-mia get-balance address) ERR_BALANCE_NOT_FOUND)) (balanceV2 (unwrap! (contract-call? .test-ccext-governance-token-nyc get-balance address) ERR_BALANCE_NOT_FOUND)) - (totalBalance (+ (* balanceV1 MICRO_CITYCOINS) balanceV2)) + ;; MAINNET (totalBalance (+ (* balanceV1 MICRO_CITYCOINS) balanceV2)) + (totalBalance (+ balanceV1 balanceV2)) ) (ok { address: address, diff --git a/models/extensions/ccd012-redemption-nyc.model.ts b/models/extensions/ccd012-redemption-nyc.model.ts index 20036bb..c115dd4 100644 --- a/models/extensions/ccd012-redemption-nyc.model.ts +++ b/models/extensions/ccd012-redemption-nyc.model.ts @@ -66,8 +66,8 @@ export class CCD012RedemptionNyc { return this.callReadOnlyFn("get-redemption-info", []); } - getNycBalances(): ReadOnlyFn { - return this.callReadOnlyFn("get-nyc-balances", []); + getNycBalances(address: string): ReadOnlyFn { + return this.callReadOnlyFn("get-nyc-balances", [types.principal(address)]); } getRedemptionForBalance(balance: number): ReadOnlyFn { @@ -78,8 +78,8 @@ export class CCD012RedemptionNyc { return this.callReadOnlyFn("get-redemption-amount-claimed", [types.principal(address)]); } - getUserRedemptionInfo(): ReadOnlyFn { - return this.callReadOnlyFn("get-user-redemption-info", []); + getUserRedemptionInfo(address: string): ReadOnlyFn { + return this.callReadOnlyFn("get-user-redemption-info", [types.principal(address)]); } // Extension callback diff --git a/tests/contracts/external/test-ccext-governance-token-mia.clar b/tests/contracts/external/test-ccext-governance-token-mia.clar index dfb5988..5e5b691 100644 --- a/tests/contracts/external/test-ccext-governance-token-mia.clar +++ b/tests/contracts/external/test-ccext-governance-token-mia.clar @@ -38,10 +38,19 @@ )) ) +;; unguarded: simple mint function (define-public (mint (amount uint) (recipient principal)) (ft-mint? miamicoin amount recipient) ) +;; guarded: burn function (by user only) +(define-public (burn (amount uint) (owner principal)) + (begin + (asserts! (or (is-eq tx-sender owner) (is-eq contract-caller owner)) ERR_NOT_TOKEN_OWNER) + (ft-burn? miamicoin amount owner) + ) +) + ;; guarded: mint governance token (define-public (edg-mint (amount uint) (recipient principal)) (begin diff --git a/tests/extensions/ccd012-redemption-nyc.test.ts b/tests/extensions/ccd012-redemption-nyc.test.ts index 6086c3b..5d2137e 100644 --- a/tests/extensions/ccd012-redemption-nyc.test.ts +++ b/tests/extensions/ccd012-redemption-nyc.test.ts @@ -190,3 +190,99 @@ Clarinet.test({ // redeem-nyc() succeeds with just v1 tokens // redeem-nyc() succeeds with just v2 tokens // redeem-nyc() succeeds with both v1 and v2 tokens + +Clarinet.test({ + name: "ccd012-redemption-nyc: redeem-nyc() succeeds with both v1 and v2 tokens", + async 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 ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccd012RedemptionNyc = new CCD012RedemptionNyc(chain, sender); + const ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(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); + + // initialize contracts + const constructBlock = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + for (let i = 0; i < constructBlock.receipts.length; i++) { + if (i === 0) { + constructBlock.receipts[i].result.expectOk().expectBool(true); + } else { + constructBlock.receipts[i].result.expectOk().expectUint(i); + } + } + + const fundV1Block = passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_002); + const fundV2Block = passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_003); + for (let i = 0; i < fundV1Block.receipts.length; i++) { + fundV1Block.receipts[i].result.expectOk().expectUint(i + 1); + fundV2Block.receipts[i].result.expectOk().expectUint(i + 1); + } + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked, lockPeriod)]); + for (let i = 0; i < stackingBlock.receipts.length; i++) { + stackingBlock.receipts[i].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 two yes votes, one no vote + const votingBlock = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user2, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user3, false)]); + for (let i = 0; i < votingBlock.receipts.length; i++) { + votingBlock.receipts[i].result.expectOk().expectBool(true); + } + + // execute ccip-022 + const executeBlock = passProposal(chain, accounts, PROPOSALS.CCIP_022); + assertEquals(executeBlock.receipts.length, 3); + for (let i = 0; i < executeBlock.receipts.length; i++) { + executeBlock.receipts[i].result.expectOk().expectUint(i + 1); + } + + // get contract redemption info + + const redemptionInfo = await ccd012RedemptionNyc.getRedemptionInfo().result; + + console.log("redemptionInfo", redemptionInfo); + + const redemptionBalanceSmall = ccd012RedemptionNyc.getRedemptionForBalance(1000).result; + const redemptionBalanceMedium = ccd012RedemptionNyc.getRedemptionForBalance(1000000).result; + const redemptionBalanceLarge = ccd012RedemptionNyc.getRedemptionForBalance(1000000000).result; + + console.log("redemptionBalanceSmall", redemptionBalanceSmall); + console.log("redemptionBalanceMedium", redemptionBalanceMedium); + console.log("redemptionBalanceLarge", redemptionBalanceLarge); + + // get user balances + const user1Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user1.address).result; + const user2Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user2.address).result; + const user3Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user3.address).result; + + console.log("user1Info", user1Info); + console.log("user2Info", user2Info); + console.log("user3Info", user3Info); + + // act + const redeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender), ccd012RedemptionNyc.redeemNyc(user1), ccd012RedemptionNyc.redeemNyc(user2), ccd012RedemptionNyc.redeemNyc(user3)]); + console.log("redeem block", redeemBlock); + + // assert + assertEquals(redeemBlock.receipts.length, 4); + for (let i = 0; i < redeemBlock.receipts.length; i++) { + redeemBlock.receipts[i].result.expectOk().expectBool(true); + } + }, +}); From e7e253d4af1b78f8c29f0e56666825c9b0f85c2d Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 22 May 2024 16:03:00 -0700 Subject: [PATCH 29/65] fix: update test assertions for redeemBlock --- tests/extensions/ccd012-redemption-nyc.test.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/extensions/ccd012-redemption-nyc.test.ts b/tests/extensions/ccd012-redemption-nyc.test.ts index 5d2137e..37f635c 100644 --- a/tests/extensions/ccd012-redemption-nyc.test.ts +++ b/tests/extensions/ccd012-redemption-nyc.test.ts @@ -199,6 +199,7 @@ Clarinet.test({ const user1 = accounts.get("wallet_1")!; const user2 = accounts.get("wallet_2")!; const user3 = accounts.get("wallet_3")!; + const user4 = accounts.get("wallet_4")!; const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); const ccd012RedemptionNyc = new CCD012RedemptionNyc(chain, sender); const ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(chain, sender); @@ -276,13 +277,17 @@ Clarinet.test({ console.log("user3Info", user3Info); // act - const redeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender), ccd012RedemptionNyc.redeemNyc(user1), ccd012RedemptionNyc.redeemNyc(user2), ccd012RedemptionNyc.redeemNyc(user3)]); + const redeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender), ccd012RedemptionNyc.redeemNyc(user1), ccd012RedemptionNyc.redeemNyc(user2), ccd012RedemptionNyc.redeemNyc(user3), ccd012RedemptionNyc.redeemNyc(user4)]); console.log("redeem block", redeemBlock); // assert - assertEquals(redeemBlock.receipts.length, 4); + assertEquals(redeemBlock.receipts.length, 5); for (let i = 0; i < redeemBlock.receipts.length; i++) { - redeemBlock.receipts[i].result.expectOk().expectBool(true); + if (i === 0) { + redeemBlock.receipts[i].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_BALANCE_NOT_FOUND); + } else { + redeemBlock.receipts[i].result.expectOk().expectBool(true); + } } }, }); From c3c1c725e1bfbd87f14c1c516ba356ef0cb8c879 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 22 May 2024 16:15:57 -0700 Subject: [PATCH 30/65] fix: match redemption amounts with contract values --- .../extensions/ccd012-redemption-nyc.test.ts | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/tests/extensions/ccd012-redemption-nyc.test.ts b/tests/extensions/ccd012-redemption-nyc.test.ts index 37f635c..797ac9f 100644 --- a/tests/extensions/ccd012-redemption-nyc.test.ts +++ b/tests/extensions/ccd012-redemption-nyc.test.ts @@ -191,6 +191,29 @@ Clarinet.test({ // redeem-nyc() succeeds with just v2 tokens // redeem-nyc() succeeds with both v1 and v2 tokens +function parseClarityTuple(clarityString) { + // Step 1: Remove the outer (ok ) and the closing parenthesis + let jsonString = clarityString.replace("(ok ", "").replace(")", ""); + + // Step 2: Add quotes around keys + jsonString = jsonString.replace(/([a-zA-Z0-9_]+):/g, '"$1":'); + + // Step 3: Add quotes around string values (addresses) + jsonString = jsonString.replace(/: ([a-zA-Z0-9_]+)/g, ': "$1"'); + + // Step 4: Remove 'u' prefix from integers + jsonString = jsonString.replace(/u([0-9]+)/g, "$1"); + + // Parse the JSON string to object + return JSON.parse(jsonString); +} + +// Example usage +const clarityString = "(ok {address: ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5, nycBalances: {address: ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5, balanceV1: u1000000, balanceV2: u999500, totalBalance: u1999500}, redemptionAmount: u99975000000, redemptionClaims: u0})"; +const userInfoObject = parseClarityTuple(clarityString); + +console.log(userInfoObject); + Clarinet.test({ name: "ccd012-redemption-nyc: redeem-nyc() succeeds with both v1 and v2 tokens", async fn(chain: Chain, accounts: Map) { @@ -256,25 +279,20 @@ Clarinet.test({ // get contract redemption info const redemptionInfo = await ccd012RedemptionNyc.getRedemptionInfo().result; - console.log("redemptionInfo", redemptionInfo); - const redemptionBalanceSmall = ccd012RedemptionNyc.getRedemptionForBalance(1000).result; - const redemptionBalanceMedium = ccd012RedemptionNyc.getRedemptionForBalance(1000000).result; - const redemptionBalanceLarge = ccd012RedemptionNyc.getRedemptionForBalance(1000000000).result; - - console.log("redemptionBalanceSmall", redemptionBalanceSmall); - console.log("redemptionBalanceMedium", redemptionBalanceMedium); - console.log("redemptionBalanceLarge", redemptionBalanceLarge); - // get user balances const user1Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user1.address).result; const user2Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user2.address).result; const user3Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user3.address).result; + const user4Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user4.address).result; + + const user1InfoObject = parseClarityTuple(user1Info); + const user2InfoObject = parseClarityTuple(user2Info); + const user3InfoObject = parseClarityTuple(user3Info); + const user4InfoObject = parseClarityTuple(user4Info); - console.log("user1Info", user1Info); - console.log("user2Info", user2Info); - console.log("user3Info", user3Info); + const userInfoObjects = [user1InfoObject, user2InfoObject, user3InfoObject, user4InfoObject]; // act const redeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender), ccd012RedemptionNyc.redeemNyc(user1), ccd012RedemptionNyc.redeemNyc(user2), ccd012RedemptionNyc.redeemNyc(user3), ccd012RedemptionNyc.redeemNyc(user4)]); @@ -286,7 +304,7 @@ Clarinet.test({ if (i === 0) { redeemBlock.receipts[i].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_BALANCE_NOT_FOUND); } else { - redeemBlock.receipts[i].result.expectOk().expectBool(true); + redeemBlock.receipts[i].result.expectOk().expectUint(userInfoObjects[i - 1].redemptionAmount); } } }, From 79a80f2833cbb218afcb0b3086ae6b6f23141ef3 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 29 May 2024 09:58:29 -0700 Subject: [PATCH 31/65] chore: remove extra console logging --- tests/extensions/ccd012-redemption-nyc.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/extensions/ccd012-redemption-nyc.test.ts b/tests/extensions/ccd012-redemption-nyc.test.ts index 797ac9f..8680920 100644 --- a/tests/extensions/ccd012-redemption-nyc.test.ts +++ b/tests/extensions/ccd012-redemption-nyc.test.ts @@ -212,7 +212,7 @@ function parseClarityTuple(clarityString) { const clarityString = "(ok {address: ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5, nycBalances: {address: ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5, balanceV1: u1000000, balanceV2: u999500, totalBalance: u1999500}, redemptionAmount: u99975000000, redemptionClaims: u0})"; const userInfoObject = parseClarityTuple(clarityString); -console.log(userInfoObject); +// console.log(userInfoObject); Clarinet.test({ name: "ccd012-redemption-nyc: redeem-nyc() succeeds with both v1 and v2 tokens", @@ -279,7 +279,7 @@ Clarinet.test({ // get contract redemption info const redemptionInfo = await ccd012RedemptionNyc.getRedemptionInfo().result; - console.log("redemptionInfo", redemptionInfo); + // console.log("redemptionInfo", redemptionInfo); // get user balances const user1Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user1.address).result; @@ -296,7 +296,7 @@ Clarinet.test({ // act const redeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender), ccd012RedemptionNyc.redeemNyc(user1), ccd012RedemptionNyc.redeemNyc(user2), ccd012RedemptionNyc.redeemNyc(user3), ccd012RedemptionNyc.redeemNyc(user4)]); - console.log("redeem block", redeemBlock); + // console.log("redeem block", redeemBlock); // assert assertEquals(redeemBlock.receipts.length, 5); From f287f456459a9b280d24ff1957b66208ea0e16e4 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 29 May 2024 10:02:40 -0700 Subject: [PATCH 32/65] fix: create new token to simulate v1 nyc To ensure everythign works correctly we should use something different than the MIA token used in current tests, and this new token will have 0 decimals like the original NYC V1 to make sure everything is calculated correctly. --- Clarinet.toml | 3 + .../test-ccext-governance-token-nyc-v1.clar | 150 ++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 tests/contracts/external/test-ccext-governance-token-nyc-v1.clar diff --git a/Clarinet.toml b/Clarinet.toml index b8b2e53..2a3880f 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -218,6 +218,9 @@ path = "tests/contracts/external/test-ccext-governance-token-mia.clar" [contracts.test-ccext-governance-token-nyc] path = "tests/contracts/external/test-ccext-governance-token-nyc.clar" +[contracts.test-ccext-governance-token-nyc-v1] +path = "tests/contracts/external/test-ccext-governance-token-nyc-v1.clar" + [contracts.test-ccext-nft-mia] path = "tests/contracts/external/test-ccext-nft-mia.clar" diff --git a/tests/contracts/external/test-ccext-governance-token-nyc-v1.clar b/tests/contracts/external/test-ccext-governance-token-nyc-v1.clar new file mode 100644 index 0000000..2e56606 --- /dev/null +++ b/tests/contracts/external/test-ccext-governance-token-nyc-v1.clar @@ -0,0 +1,150 @@ +;; Title: Governance Token +;; Version: 0.0.0 +;; Synopsis: Simple SIP-010 asset for clarinet testing. +;; Description: +;; A dummy SIP-010 asset for use in tests. + +;; TRAITS + +(impl-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait) + +;; TOKEN DEFINITIONS + +(define-fungible-token newyorkcitycoin) +(define-fungible-token newyorkcitycoin-locked) + +;; CONSTANTS + +;; error codes +(define-constant ERR_UNAUTHORIZED (err u3000)) +(define-constant ERR_NOT_TOKEN_OWNER (err u4)) + +;; DATA VARS + +(define-data-var token-name (string-ascii 32) "CityCoins Governance Token V1") +(define-data-var token-symbol (string-ascii 10) "NYC") +(define-data-var token-uri (optional (string-utf8 256)) none) +(define-data-var token-decimals uint u0) + +;; PUBLIC FUNCTIONS + +;; authorization check +(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 + )) +) + +;; unguarded: simple mint function +(define-public (mint (amount uint) (recipient principal)) + (ft-mint? newyorkcitycoin amount recipient) +) + +;; guarded: burn function (by user only) +(define-public (burn (amount uint) (owner principal)) + (begin + (asserts! (or (is-eq tx-sender owner) (is-eq contract-caller owner)) ERR_NOT_TOKEN_OWNER) + (ft-burn? newyorkcitycoin amount owner) + ) +) + +;; guarded: mint governance token +(define-public (edg-mint (amount uint) (recipient principal)) + (begin + (try! (is-dao-or-extension)) + (ft-mint? newyorkcitycoin amount recipient) + ) +) + +;; guarded: mint governance token to multiple recipients +(define-public (edg-mint-many (recipients (list 200 {amount: uint, recipient: principal}))) + (begin + (try! (is-dao-or-extension)) + (ok (map edg-mint-many-iter recipients)) + ) +) + +;; guarded: burn governance token +(define-public (edg-burn (amount uint) (owner principal)) + (begin + (try! (is-dao-or-extension)) + (ft-burn? newyorkcitycoin amount owner) + ) +) + +;; guarded: set token name +(define-public (set-name (new-name (string-ascii 32))) + (begin + (try! (is-dao-or-extension)) + (ok (var-set token-name new-name)) + ) +) + +;; guarded: set token symbol +(define-public (set-symbol (new-symbol (string-ascii 10))) + (begin + (try! (is-dao-or-extension)) + (ok (var-set token-symbol new-symbol)) + ) +) + +;; guarded: set token decimals +(define-public (set-decimals (new-decimals uint)) + (begin + (try! (is-dao-or-extension)) + (ok (var-set token-decimals new-decimals)) + ) +) + +;; guarded: set token uri +(define-public (set-token-uri (new-uri (optional (string-utf8 256)))) + (begin + (try! (is-dao-or-extension)) + (ok (var-set token-uri new-uri)) + ) +) + +;; SIP-010: transfer +(define-public (transfer (amount uint) (sender principal) (recipient principal) (memo (optional (buff 34)))) + (begin + (asserts! (or (is-eq tx-sender sender) (is-eq contract-caller sender)) ERR_NOT_TOKEN_OWNER) + (ft-transfer? newyorkcitycoin amount sender recipient) + ) +) + +;; READ ONLY FUNCTIONS + +;; SIP-010 functions + +(define-read-only (get-name) + (ok (var-get token-name)) +) + +(define-read-only (get-symbol) + (ok (var-get token-symbol)) +) + +(define-read-only (get-decimals) + (ok (var-get token-decimals)) +) + +(define-read-only (get-balance (who principal)) + (ok (+ (ft-get-balance newyorkcitycoin who) (ft-get-balance newyorkcitycoin-locked who))) +) + +(define-read-only (get-total-supply) + (ok (+ (ft-get-supply newyorkcitycoin) (ft-get-supply newyorkcitycoin-locked))) +) + +(define-read-only (get-token-uri) + (ok (var-get token-uri)) +) + +;; PRIVATE FUNCTIONS + +(define-private (edg-mint-many-iter (item {amount: uint, recipient: principal})) + (ft-mint? newyorkcitycoin (get amount item) (get recipient item)) +) From 4327fe2e1d05cac7a87ad2d9d9814184377bffcc Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 29 May 2024 10:22:52 -0700 Subject: [PATCH 33/65] fix: use new nyc-v1 with 0 decimals instead of mia --- contracts/extensions/ccd012-redemption-nyc.clar | 17 +++++++---------- ...est-ccip022-treasury-redemption-nyc-001.clar | 6 +++--- ...est-ccip022-treasury-redemption-nyc-002.clar | 8 ++++---- ...est-ccip022-treasury-redemption-nyc-003.clar | 8 ++++---- utils/common.ts | 1 + 5 files changed, 19 insertions(+), 21 deletions(-) diff --git a/contracts/extensions/ccd012-redemption-nyc.clar b/contracts/extensions/ccd012-redemption-nyc.clar index eeb1cf4..d8e6601 100644 --- a/contracts/extensions/ccd012-redemption-nyc.clar +++ b/contracts/extensions/ccd012-redemption-nyc.clar @@ -57,10 +57,9 @@ ( ;; MAINNET: SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token ;; MAINNET: SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 - (nycTotalSupplyV1 (unwrap! (contract-call? .test-ccext-governance-token-mia get-total-supply) ERR_PANIC)) + (nycTotalSupplyV1 (unwrap! (contract-call? .test-ccext-governance-token-nyc-v1 get-total-supply) ERR_PANIC)) (nycTotalSupplyV2 (unwrap! (contract-call? .test-ccext-governance-token-nyc get-total-supply) ERR_PANIC)) - ;; MAINNET: (nycTotalSupply (+ (* nycTotalSupplyV1 MICRO_CITYCOINS) nycTotalSupplyV2)) - (nycTotalSupply (+ nycTotalSupplyV1 nycTotalSupplyV2)) + (nycTotalSupply (+ (* nycTotalSupplyV1 MICRO_CITYCOINS) nycTotalSupplyV2)) (nycRedemptionBalance (as-contract (stx-get-balance tx-sender))) ) ;; check if sender is DAO or extension @@ -92,10 +91,9 @@ (userAddress tx-sender) ;; MAINNET: SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token ;; MAINNET: SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 - (balanceV1 (unwrap! (contract-call? .test-ccext-governance-token-mia get-balance userAddress) ERR_BALANCE_NOT_FOUND)) + (balanceV1 (unwrap! (contract-call? .test-ccext-governance-token-nyc-v1 get-balance userAddress) ERR_BALANCE_NOT_FOUND)) (balanceV2 (unwrap! (contract-call? .test-ccext-governance-token-nyc get-balance userAddress) ERR_BALANCE_NOT_FOUND)) - ;; MAINNET: (totalBalance (+ (* balanceV1 MICRO_CITYCOINS) balanceV2)) - (totalBalance (+ balanceV1 balanceV2)) + (totalBalance (+ (* balanceV1 MICRO_CITYCOINS) balanceV2)) (redemptionAmount (unwrap! (get-redemption-for-balance totalBalance) ERR_NOTHING_TO_REDEEM)) (redemptionClaims (default-to u0 (get-redemption-amount-claimed userAddress))) ) @@ -110,7 +108,7 @@ ;; burn NYC ;; MAINNET: SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token ;; MAINNET: SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 - (and (> u0 balanceV1) (try! (contract-call? .test-ccext-governance-token-mia burn balanceV1 userAddress))) + (and (> u0 balanceV1) (try! (contract-call? .test-ccext-governance-token-nyc-v1 burn balanceV1 userAddress))) (and (> u0 balanceV2) (try! (contract-call? .test-ccext-governance-token-nyc burn balanceV2 userAddress))) ;; transfer STX (try! (as-contract (stx-transfer? redemptionAmount tx-sender userAddress))) @@ -163,10 +161,9 @@ ( ;; MAINNET: SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token ;; MAINNET: SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 - (balanceV1 (unwrap! (contract-call? .test-ccext-governance-token-mia get-balance address) ERR_BALANCE_NOT_FOUND)) + (balanceV1 (unwrap! (contract-call? .test-ccext-governance-token-nyc-v1 get-balance address) ERR_BALANCE_NOT_FOUND)) (balanceV2 (unwrap! (contract-call? .test-ccext-governance-token-nyc get-balance address) ERR_BALANCE_NOT_FOUND)) - ;; MAINNET (totalBalance (+ (* balanceV1 MICRO_CITYCOINS) balanceV2)) - (totalBalance (+ balanceV1 balanceV2)) + (totalBalance (+ (* balanceV1 MICRO_CITYCOINS) balanceV2)) ) (ok { address: address, diff --git a/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-001.clar b/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-001.clar index 226f4ad..7774abf 100644 --- a/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-001.clar +++ b/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-001.clar @@ -37,7 +37,7 @@ (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-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-v1 true)) (try! (contract-call? .ccd002-treasury-nyc-stacking set-allowed .test-ccext-governance-token-nyc true)) ;; test-ccd005-city-data-009 (try! (contract-call? .ccd005-city-data set-coinbase-amounts u1 u10 u100 u1000 u10000 u100000 u1000000 u10000000)) @@ -53,5 +53,5 @@ ) ) -;; transfer 1M STX to mining treasury contract from deployer -(stx-transfer? u1000000000000 tx-sender .ccd002-treasury-nyc-mining-v2) +;; transfer 15M STX to mining treasury contract from deployer +(stx-transfer? u15000000000000 tx-sender .ccd002-treasury-nyc-mining-v2) diff --git a/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-002.clar b/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-002.clar index 1c9855b..054afd5 100644 --- a/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-002.clar +++ b/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-002.clar @@ -9,10 +9,10 @@ (define-public (execute (sender principal)) (begin ;; to simulate newyorkcitycoin-token - (try! (contract-call? .test-ccext-governance-token-mia mint u1000000 'ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5)) - (try! (contract-call? .test-ccext-governance-token-mia mint u2000000 'ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG)) - (try! (contract-call? .test-ccext-governance-token-mia mint u3000000 'ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC)) - (try! (contract-call? .test-ccext-governance-token-mia mint u4000000 'ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND)) + (try! (contract-call? .test-ccext-governance-token-nyc-v1 mint u10000 'ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5)) ;; 10k NYC + (try! (contract-call? .test-ccext-governance-token-nyc-v1 mint u1000000 'ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG)) ;; 1M NYC + (try! (contract-call? .test-ccext-governance-token-nyc-v1 mint u5000000 'ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC)) ;; 5M NYC + (try! (contract-call? .test-ccext-governance-token-nyc-v1 mint u10000000 'ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND)) ;; 10M NYC (ok true) ) ) diff --git a/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-003.clar b/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-003.clar index e2308b3..c840075 100644 --- a/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-003.clar +++ b/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-003.clar @@ -9,10 +9,10 @@ (define-public (execute (sender principal)) (begin ;; to simulate newyorkcitycoin-token-v2 - (try! (contract-call? .test-ccext-governance-token-nyc mint u1000000 'ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5)) - (try! (contract-call? .test-ccext-governance-token-nyc mint u2000000 'ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG)) - (try! (contract-call? .test-ccext-governance-token-nyc mint u3000000 'ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC)) - (try! (contract-call? .test-ccext-governance-token-nyc mint u4000000 'ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND)) + (try! (contract-call? .test-ccext-governance-token-nyc mint u10000000000 'ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5)) ;; 10K NYC + (try! (contract-call? .test-ccext-governance-token-nyc mint u1000000000000 'ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG)) ;; 1M NYC + (try! (contract-call? .test-ccext-governance-token-nyc mint u5000000000000 'ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC)) ;; 5M NYC + (try! (contract-call? .test-ccext-governance-token-nyc mint u10000000000000 'ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND)) ;; 10M NYC (ok true) ) ) diff --git a/utils/common.ts b/utils/common.ts index eaad8c3..40c7644 100644 --- a/utils/common.ts +++ b/utils/common.ts @@ -130,6 +130,7 @@ export const PROPOSALS = { export const EXTERNAL = { FT_MIA: ADDRESS.concat(".test-ccext-governance-token-mia"), FT_NYC: ADDRESS.concat(".test-ccext-governance-token-nyc"), + FT_NYC_V1: ADDRESS.concat(".test-ccext-governance-token-nyc-v1"), NFT_MIA: ADDRESS.concat(".test-ccext-nft-mia"), NFT_NYC: ADDRESS.concat(".test-ccext-nft-nyc"), }; From 160c2475f14434106d5b3b7413967e1055515958 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 29 May 2024 10:23:06 -0700 Subject: [PATCH 34/65] fix: add tests for v1/v2 tokens only --- .../extensions/ccd012-redemption-nyc.test.ts | 197 +++++++++++++++++- 1 file changed, 188 insertions(+), 9 deletions(-) diff --git a/tests/extensions/ccd012-redemption-nyc.test.ts b/tests/extensions/ccd012-redemption-nyc.test.ts index 8680920..478e1d1 100644 --- a/tests/extensions/ccd012-redemption-nyc.test.ts +++ b/tests/extensions/ccd012-redemption-nyc.test.ts @@ -187,9 +187,6 @@ Clarinet.test({ // redeem-nyc() fails with ERR_ALREADY_CLAIMED if the redemption is already claimed // redeem-nyc() fails with ERR_BALANCE_NOT_FOUND if v1 or v2 tokens are not found // redeem-nyc() fails with ERR_NOTHING_TO_REDEEM if the redemption amount is 0 -// redeem-nyc() succeeds with just v1 tokens -// redeem-nyc() succeeds with just v2 tokens -// redeem-nyc() succeeds with both v1 and v2 tokens function parseClarityTuple(clarityString) { // Step 1: Remove the outer (ok ) and the closing parenthesis @@ -208,11 +205,193 @@ function parseClarityTuple(clarityString) { return JSON.parse(jsonString); } -// Example usage -const clarityString = "(ok {address: ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5, nycBalances: {address: ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5, balanceV1: u1000000, balanceV2: u999500, totalBalance: u1999500}, redemptionAmount: u99975000000, redemptionClaims: u0})"; -const userInfoObject = parseClarityTuple(clarityString); +Clarinet.test({ + name: "ccd012-redemption-nyc: redeem-nyc() succeeds with only v1 tokens", + async 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 user4 = accounts.get("wallet_4")!; + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccd012RedemptionNyc = new CCD012RedemptionNyc(chain, sender); + const ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(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); + + // initialize contracts + const constructBlock = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + for (let i = 0; i < constructBlock.receipts.length; i++) { + if (i === 0) { + constructBlock.receipts[i].result.expectOk().expectBool(true); + } else { + constructBlock.receipts[i].result.expectOk().expectUint(i); + } + } + + const fundV1Block = passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_002); + for (let i = 0; i < fundV1Block.receipts.length; i++) { + fundV1Block.receipts[i].result.expectOk().expectUint(i + 1); + } + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked, lockPeriod)]); + for (let i = 0; i < stackingBlock.receipts.length; i++) { + stackingBlock.receipts[i].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 two yes votes, one no vote + const votingBlock = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user2, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user3, false)]); + for (let i = 0; i < votingBlock.receipts.length; i++) { + votingBlock.receipts[i].result.expectOk().expectBool(true); + } + + // execute ccip-022 + const executeBlock = passProposal(chain, accounts, PROPOSALS.CCIP_022); + assertEquals(executeBlock.receipts.length, 3); + for (let i = 0; i < executeBlock.receipts.length; i++) { + executeBlock.receipts[i].result.expectOk().expectUint(i + 1); + } + + // get contract redemption info + + const redemptionInfo = await ccd012RedemptionNyc.getRedemptionInfo().result; + console.log("v1 only redemptionInfo", redemptionInfo); + + // get user balances + const user1Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user1.address).result; + const user2Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user2.address).result; + const user3Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user3.address).result; + const user4Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user4.address).result; + + const user1InfoObject = parseClarityTuple(user1Info); + const user2InfoObject = parseClarityTuple(user2Info); + const user3InfoObject = parseClarityTuple(user3Info); + const user4InfoObject = parseClarityTuple(user4Info); + + const userInfoObjects = [user1InfoObject, user2InfoObject, user3InfoObject, user4InfoObject]; + + // act + const redeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender), ccd012RedemptionNyc.redeemNyc(user1), ccd012RedemptionNyc.redeemNyc(user2), ccd012RedemptionNyc.redeemNyc(user3), ccd012RedemptionNyc.redeemNyc(user4)]); + console.log("v1 only redeem block", redeemBlock); + + // assert + assertEquals(redeemBlock.receipts.length, 5); + for (let i = 0; i < redeemBlock.receipts.length; i++) { + if (i === 0) { + redeemBlock.receipts[i].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_BALANCE_NOT_FOUND); + } else { + redeemBlock.receipts[i].result.expectOk().expectUint(userInfoObjects[i - 1].redemptionAmount); + } + } + }, +}); -// console.log(userInfoObject); +Clarinet.test({ + name: "ccd012-redemption-nyc: redeem-nyc() succeeds with only v2 tokens", + async 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 user4 = accounts.get("wallet_4")!; + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccd012RedemptionNyc = new CCD012RedemptionNyc(chain, sender); + const ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(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); + + // initialize contracts + const constructBlock = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + for (let i = 0; i < constructBlock.receipts.length; i++) { + if (i === 0) { + constructBlock.receipts[i].result.expectOk().expectBool(true); + } else { + constructBlock.receipts[i].result.expectOk().expectUint(i); + } + } + + const fundV2Block = passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_003); + for (let i = 0; i < fundV2Block.receipts.length; i++) { + fundV2Block.receipts[i].result.expectOk().expectUint(i + 1); + } + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked, lockPeriod)]); + for (let i = 0; i < stackingBlock.receipts.length; i++) { + stackingBlock.receipts[i].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 two yes votes, one no vote + const votingBlock = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user2, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user3, false)]); + for (let i = 0; i < votingBlock.receipts.length; i++) { + votingBlock.receipts[i].result.expectOk().expectBool(true); + } + + // execute ccip-022 + const executeBlock = passProposal(chain, accounts, PROPOSALS.CCIP_022); + assertEquals(executeBlock.receipts.length, 3); + for (let i = 0; i < executeBlock.receipts.length; i++) { + executeBlock.receipts[i].result.expectOk().expectUint(i + 1); + } + + // get contract redemption info + + const redemptionInfo = await ccd012RedemptionNyc.getRedemptionInfo().result; + console.log("v2 only redemptionInfo", redemptionInfo); + + // get user balances + const user1Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user1.address).result; + const user2Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user2.address).result; + const user3Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user3.address).result; + const user4Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user4.address).result; + + const user1InfoObject = parseClarityTuple(user1Info); + const user2InfoObject = parseClarityTuple(user2Info); + const user3InfoObject = parseClarityTuple(user3Info); + const user4InfoObject = parseClarityTuple(user4Info); + + const userInfoObjects = [user1InfoObject, user2InfoObject, user3InfoObject, user4InfoObject]; + + // act + const redeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender), ccd012RedemptionNyc.redeemNyc(user1), ccd012RedemptionNyc.redeemNyc(user2), ccd012RedemptionNyc.redeemNyc(user3), ccd012RedemptionNyc.redeemNyc(user4)]); + console.log("v2 only redeem block", redeemBlock); + + // assert + assertEquals(redeemBlock.receipts.length, 5); + for (let i = 0; i < redeemBlock.receipts.length; i++) { + if (i === 0) { + redeemBlock.receipts[i].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_BALANCE_NOT_FOUND); + } else { + redeemBlock.receipts[i].result.expectOk().expectUint(userInfoObjects[i - 1].redemptionAmount); + } + } + }, +}); Clarinet.test({ name: "ccd012-redemption-nyc: redeem-nyc() succeeds with both v1 and v2 tokens", @@ -279,7 +458,7 @@ Clarinet.test({ // get contract redemption info const redemptionInfo = await ccd012RedemptionNyc.getRedemptionInfo().result; - // console.log("redemptionInfo", redemptionInfo); + console.log("redemptionInfo", redemptionInfo); // get user balances const user1Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user1.address).result; @@ -296,7 +475,7 @@ Clarinet.test({ // act const redeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender), ccd012RedemptionNyc.redeemNyc(user1), ccd012RedemptionNyc.redeemNyc(user2), ccd012RedemptionNyc.redeemNyc(user3), ccd012RedemptionNyc.redeemNyc(user4)]); - // console.log("redeem block", redeemBlock); + console.log("redeem block", redeemBlock); // assert assertEquals(redeemBlock.receipts.length, 5); From 69dd5d10406064903e34e8f5cc967d5db384490c Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 29 May 2024 10:25:22 -0700 Subject: [PATCH 35/65] fix: move parsing function to common utils --- .../extensions/ccd012-redemption-nyc.test.ts | 19 +------------------ utils/common.ts | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/tests/extensions/ccd012-redemption-nyc.test.ts b/tests/extensions/ccd012-redemption-nyc.test.ts index 478e1d1..3e1f52f 100644 --- a/tests/extensions/ccd012-redemption-nyc.test.ts +++ b/tests/extensions/ccd012-redemption-nyc.test.ts @@ -1,7 +1,7 @@ import { CCD007CityStacking } from "../../models/extensions/ccd007-citycoin-stacking.model.ts"; import { CCD012RedemptionNyc } from "../../models/extensions/ccd012-redemption-nyc.model.ts"; import { CCIP022TreasuryRedemptionNYC } from "../../models/proposals/ccip022-treasury-redemption-nyc.model.ts"; -import { EXTENSIONS, PROPOSALS, constructAndPassProposal, nyc, passProposal } from "../../utils/common.ts"; +import { EXTENSIONS, PROPOSALS, constructAndPassProposal, nyc, parseClarityTuple, passProposal } from "../../utils/common.ts"; import { Account, assertEquals, Clarinet, Chain, types } from "../../utils/deps.ts"; // ============================= @@ -188,23 +188,6 @@ Clarinet.test({ // redeem-nyc() fails with ERR_BALANCE_NOT_FOUND if v1 or v2 tokens are not found // redeem-nyc() fails with ERR_NOTHING_TO_REDEEM if the redemption amount is 0 -function parseClarityTuple(clarityString) { - // Step 1: Remove the outer (ok ) and the closing parenthesis - let jsonString = clarityString.replace("(ok ", "").replace(")", ""); - - // Step 2: Add quotes around keys - jsonString = jsonString.replace(/([a-zA-Z0-9_]+):/g, '"$1":'); - - // Step 3: Add quotes around string values (addresses) - jsonString = jsonString.replace(/: ([a-zA-Z0-9_]+)/g, ': "$1"'); - - // Step 4: Remove 'u' prefix from integers - jsonString = jsonString.replace(/u([0-9]+)/g, "$1"); - - // Parse the JSON string to object - return JSON.parse(jsonString); -} - Clarinet.test({ name: "ccd012-redemption-nyc: redeem-nyc() succeeds with only v1 tokens", async fn(chain: Chain, accounts: Map) { diff --git a/utils/common.ts b/utils/common.ts index 40c7644..fb0c68c 100644 --- a/utils/common.ts +++ b/utils/common.ts @@ -243,3 +243,21 @@ export const nyc: CityData = { treasuryV2Id: 2, treasuryV2Name: "mining-v2", }; + +// parses an (ok ...) response into a JS object +export function parseClarityTuple(clarityString) { + // Step 1: Remove the outer (ok ) and the closing parenthesis + let jsonString = clarityString.replace("(ok ", "").replace(")", ""); + + // Step 2: Add quotes around keys + jsonString = jsonString.replace(/([a-zA-Z0-9_]+):/g, '"$1":'); + + // Step 3: Add quotes around string values (addresses) + jsonString = jsonString.replace(/: ([a-zA-Z0-9_]+)/g, ': "$1"'); + + // Step 4: Remove 'u' prefix from integers + jsonString = jsonString.replace(/u([0-9]+)/g, "$1"); + + // Parse the JSON string to object + return JSON.parse(jsonString); +} From 2c033a9cf63c39e7a44ef30f8fdd0901cbe62d38 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 29 May 2024 10:36:17 -0700 Subject: [PATCH 36/65] fix: add test proposal to mint/stack v2 NYC through deployer In the case of testing V1 token claims someone still needs to stack and vote on CCIP-022. This mints a small amount of NYC to the deployer account to do this, while the tests use the amounts minted to users 1-4 to evaluate the redemption ratio and result. --- Clarinet.toml | 5 +++++ ...test-ccip022-treasury-redemption-nyc-005.clar | 16 ++++++++++++++++ tests/extensions/ccd012-redemption-nyc.test.ts | 11 ++++++++--- utils/common.ts | 1 + 4 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-005.clar diff --git a/Clarinet.toml b/Clarinet.toml index 2a3880f..a16a86e 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -555,6 +555,11 @@ path = "tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-004.clar" clarity_version = 2 epoch = 2.4 +[contracts.test-ccip022-treasury-redemption-nyc-005] +path = "tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-005.clar" +clarity_version = 2 +epoch = 2.4 + [repl] costs_version = 2 parser_version = 2 diff --git a/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-005.clar b/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-005.clar new file mode 100644 index 0000000..05da6f8 --- /dev/null +++ b/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-005.clar @@ -0,0 +1,16 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer +;; Description: +;; Mints 10K NYC V2 to deployer address + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (begin + ;; to simulate newyorkcitycoin-token-v2 + (try! (contract-call? .test-ccext-governance-token-nyc mint u10000000000 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM)) ;; 10K NYC + (ok true) + ) +) + diff --git a/tests/extensions/ccd012-redemption-nyc.test.ts b/tests/extensions/ccd012-redemption-nyc.test.ts index 3e1f52f..955e235 100644 --- a/tests/extensions/ccd012-redemption-nyc.test.ts +++ b/tests/extensions/ccd012-redemption-nyc.test.ts @@ -219,12 +219,15 @@ Clarinet.test({ } const fundV1Block = passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_002); + const fundV2SenderBlock = passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_005); for (let i = 0; i < fundV1Block.receipts.length; i++) { fundV1Block.receipts[i].result.expectOk().expectUint(i + 1); + fundV2SenderBlock.receipts[i].result.expectOk().expectUint(i + 1); } // stack first cycle u1, last cycle u10 - const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked, lockPeriod)]); + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(sender, nyc.cityName, amountStacked, lockPeriod)]); + // console.log("stackingBlock", stackingBlock); for (let i = 0; i < stackingBlock.receipts.length; i++) { stackingBlock.receipts[i].result.expectOk().expectBool(true); } @@ -235,14 +238,16 @@ Clarinet.test({ chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); - // execute two yes votes, one no vote - const votingBlock = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user2, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user3, false)]); + // execute one yes vote + const votingBlock = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(sender, true)]); + // console.log("votingBlock", votingBlock); for (let i = 0; i < votingBlock.receipts.length; i++) { votingBlock.receipts[i].result.expectOk().expectBool(true); } // execute ccip-022 const executeBlock = passProposal(chain, accounts, PROPOSALS.CCIP_022); + // console.log("executeBlock", executeBlock); assertEquals(executeBlock.receipts.length, 3); for (let i = 0; i < executeBlock.receipts.length; i++) { executeBlock.receipts[i].result.expectOk().expectUint(i + 1); diff --git a/utils/common.ts b/utils/common.ts index fb0c68c..e302729 100644 --- a/utils/common.ts +++ b/utils/common.ts @@ -125,6 +125,7 @@ export const PROPOSALS = { TEST_CCIP022_TREASURY_REDEMPTION_NYC_002: ADDRESS.concat(".test-ccip022-treasury-redemption-nyc-002"), TEST_CCIP022_TREASURY_REDEMPTION_NYC_003: ADDRESS.concat(".test-ccip022-treasury-redemption-nyc-003"), TEST_CCIP022_TREASURY_REDEMPTION_NYC_004: ADDRESS.concat(".test-ccip022-treasury-redemption-nyc-004"), + TEST_CCIP022_TREASURY_REDEMPTION_NYC_005: ADDRESS.concat(".test-ccip022-treasury-redemption-nyc-005"), }; export const EXTERNAL = { From 686b793d727d12d4cdfe1e68181f8c8723e717cd Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 29 May 2024 10:44:20 -0700 Subject: [PATCH 37/65] fix: update print event to match latest details This will need to be updated again, there is a bug with the redemption ratio where the value returned is u0, once resolved this test will fail until the new value is entered. --- contracts/extensions/ccd012-redemption-nyc.clar | 2 -- tests/extensions/ccd012-redemption-nyc.test.ts | 5 +++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/contracts/extensions/ccd012-redemption-nyc.clar b/contracts/extensions/ccd012-redemption-nyc.clar index d8e6601..12d5bf1 100644 --- a/contracts/extensions/ccd012-redemption-nyc.clar +++ b/contracts/extensions/ccd012-redemption-nyc.clar @@ -99,8 +99,6 @@ ) ;; check if redemptions are enabled (asserts! (var-get redemptionsEnabled) ERR_NOT_ENABLED) - ;; check that user has not already claimed - (asserts! (is-eq redemptionClaims u0) ERR_ALREADY_CLAIMED) ;; check that user has at least one positive balance (asserts! (or (> balanceV1 u0) (> balanceV2 u0)) ERR_BALANCE_NOT_FOUND) ;; check that redemption amount is > 0 diff --git a/tests/extensions/ccd012-redemption-nyc.test.ts b/tests/extensions/ccd012-redemption-nyc.test.ts index 955e235..68d84d7 100644 --- a/tests/extensions/ccd012-redemption-nyc.test.ts +++ b/tests/extensions/ccd012-redemption-nyc.test.ts @@ -126,7 +126,8 @@ Clarinet.test({ executeBlock.receipts[1].result.expectOk().expectUint(2); executeBlock.receipts[2].result.expectOk().expectUint(3); - const expectedEvent = `{blockHeight: u${executeBlock.height - 1}, contractBalance: u1000000000000, redemptionRatio: u50000, redemptionsEnabled: true, totalSupply: u20000000}`; + const expectedEvent = `{blockHeight: u${executeBlock.height - 1}, contractBalance: u15000000000000, redemptionRatio: u0, redemptionsEnabled: true, totalSupply: u32020000000000}`; + // console.log(executeBlock.receipts[2].events[3].contract_event.value); executeBlock.receipts[2].events.expectPrintEvent(EXTENSIONS.CCD012_REDEMPTION_NYC, expectedEvent); }, }); @@ -201,7 +202,7 @@ Clarinet.test({ const ccd012RedemptionNyc = new CCD012RedemptionNyc(chain, sender); const ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(chain, sender); - const amountStacked = 500; + const amountStacked = 10000000000; // match balance for user const lockPeriod = 10; // progress the chain to avoid underflow in From 76b36d8ab07e671e9748d97da641fff0729abcf7 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 29 May 2024 10:53:47 -0700 Subject: [PATCH 38/65] fix: add test for second claim after unstacking --- .../extensions/ccd012-redemption-nyc.test.ts | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/tests/extensions/ccd012-redemption-nyc.test.ts b/tests/extensions/ccd012-redemption-nyc.test.ts index 68d84d7..d6b4fac 100644 --- a/tests/extensions/ccd012-redemption-nyc.test.ts +++ b/tests/extensions/ccd012-redemption-nyc.test.ts @@ -477,3 +477,130 @@ Clarinet.test({ } }, }); + +Clarinet.test({ + name: "ccd012-redemption-nyc: redeem-nyc() succeeds with additional claims after unstacking tokens", + async 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 user4 = accounts.get("wallet_4")!; + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccd012RedemptionNyc = new CCD012RedemptionNyc(chain, sender); + const ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(chain, sender); + + const amountStacked = 10000; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + + // initialize contracts + const constructBlock = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + for (let i = 0; i < constructBlock.receipts.length; i++) { + if (i === 0) { + constructBlock.receipts[i].result.expectOk().expectBool(true); + } else { + constructBlock.receipts[i].result.expectOk().expectUint(i); + } + } + + const fundV1Block = passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_002); + const fundV2Block = passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_003); + for (let i = 0; i < fundV1Block.receipts.length; i++) { + fundV1Block.receipts[i].result.expectOk().expectUint(i + 1); + fundV2Block.receipts[i].result.expectOk().expectUint(i + 1); + } + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked, lockPeriod)]); + for (let i = 0; i < stackingBlock.receipts.length; i++) { + stackingBlock.receipts[i].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 two yes votes, one no vote + const votingBlock = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user2, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user3, false)]); + for (let i = 0; i < votingBlock.receipts.length; i++) { + votingBlock.receipts[i].result.expectOk().expectBool(true); + } + + // execute ccip-022 + const executeBlock = passProposal(chain, accounts, PROPOSALS.CCIP_022); + assertEquals(executeBlock.receipts.length, 3); + for (let i = 0; i < executeBlock.receipts.length; i++) { + executeBlock.receipts[i].result.expectOk().expectUint(i + 1); + } + + // get contract redemption info + const redemptionInfo = await ccd012RedemptionNyc.getRedemptionInfo().result; + console.log("multi redemptionInfo", redemptionInfo); + + // get user balances + const user1Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user1.address).result; + const user2Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user2.address).result; + const user3Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user3.address).result; + const user4Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user4.address).result; + + const user1InfoObject = parseClarityTuple(user1Info); + const user2InfoObject = parseClarityTuple(user2Info); + const user3InfoObject = parseClarityTuple(user3Info); + const user4InfoObject = parseClarityTuple(user4Info); + + const userInfoObjects = [user1InfoObject, user2InfoObject, user3InfoObject, user4InfoObject]; + + // redeem token balances once for each user + const firstRedeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender), ccd012RedemptionNyc.redeemNyc(user1), ccd012RedemptionNyc.redeemNyc(user2), ccd012RedemptionNyc.redeemNyc(user3), ccd012RedemptionNyc.redeemNyc(user4)]); + console.log("firstRedeemBlock", firstRedeemBlock); + assertEquals(firstRedeemBlock.receipts.length, 5); + for (let i = 0; i < firstRedeemBlock.receipts.length; i++) { + if (i === 0) { + firstRedeemBlock.receipts[i].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_BALANCE_NOT_FOUND); + } else { + firstRedeemBlock.receipts[i].result.expectOk().expectUint(userInfoObjects[i - 1].redemptionAmount); + } + } + + // claim stacking rewards from cycle u10 + const stackingClaimBlock = chain.mineBlock([ccd007CityStacking.claimStackingReward(user1, nyc.cityName, 10), ccd007CityStacking.claimStackingReward(user2, nyc.cityName, 10), ccd007CityStacking.claimStackingReward(user3, nyc.cityName, 10)]); + for (let i = 0; i < stackingClaimBlock.receipts.length; i++) { + stackingClaimBlock.receipts[i].result.expectOk().expectBool(true); + } + + // act + // redeem token balances once for each user + const secondRedeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender), ccd012RedemptionNyc.redeemNyc(user1), ccd012RedemptionNyc.redeemNyc(user2), ccd012RedemptionNyc.redeemNyc(user3), ccd012RedemptionNyc.redeemNyc(user4)]); + console.log("secondRedeemBlock", secondRedeemBlock); + + // get user balances + const user1Info2 = await ccd012RedemptionNyc.getUserRedemptionInfo(user1.address).result; + const user2Info2 = await ccd012RedemptionNyc.getUserRedemptionInfo(user2.address).result; + const user3Info2 = await ccd012RedemptionNyc.getUserRedemptionInfo(user3.address).result; + const user4Info2 = await ccd012RedemptionNyc.getUserRedemptionInfo(user4.address).result; + + const user1InfoObject2 = parseClarityTuple(user1Info); + const user2InfoObject2 = parseClarityTuple(user2Info); + const user3InfoObject2 = parseClarityTuple(user3Info); + const user4InfoObject2 = parseClarityTuple(user4Info); + + const userInfoObjects2 = [user1InfoObject2, user2InfoObject2, user3InfoObject2, user4InfoObject2]; + + // assert + assertEquals(secondRedeemBlock.receipts.length, 5); + for (let i = 0; i < secondRedeemBlock.receipts.length; i++) { + if (i === 0) { + secondRedeemBlock.receipts[i].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_BALANCE_NOT_FOUND); + } else { + secondRedeemBlock.receipts[i].result.expectOk().expectUint(userInfoObjects2[i - 1].redemptionAmount); + } + } + }, +}); From 67711c82d340973605b538d62e677fa001daeae1 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 29 May 2024 10:54:14 -0700 Subject: [PATCH 39/65] chore: variable name change --- tests/extensions/ccd012-redemption-nyc.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/extensions/ccd012-redemption-nyc.test.ts b/tests/extensions/ccd012-redemption-nyc.test.ts index d6b4fac..d351c8b 100644 --- a/tests/extensions/ccd012-redemption-nyc.test.ts +++ b/tests/extensions/ccd012-redemption-nyc.test.ts @@ -586,10 +586,10 @@ Clarinet.test({ const user3Info2 = await ccd012RedemptionNyc.getUserRedemptionInfo(user3.address).result; const user4Info2 = await ccd012RedemptionNyc.getUserRedemptionInfo(user4.address).result; - const user1InfoObject2 = parseClarityTuple(user1Info); - const user2InfoObject2 = parseClarityTuple(user2Info); - const user3InfoObject2 = parseClarityTuple(user3Info); - const user4InfoObject2 = parseClarityTuple(user4Info); + const user1InfoObject2 = parseClarityTuple(user1Info2); + const user2InfoObject2 = parseClarityTuple(user2Info2); + const user3InfoObject2 = parseClarityTuple(user3Info2); + const user4InfoObject2 = parseClarityTuple(user4Info2); const userInfoObjects2 = [user1InfoObject2, user2InfoObject2, user3InfoObject2, user4InfoObject2]; From 89465124a8681a0b0b9751a6539a1eeba688d4d5 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 29 May 2024 10:58:45 -0700 Subject: [PATCH 40/65] fix: add fixed point math helpers --- contracts/extensions/ccd012-redemption-nyc.clar | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/contracts/extensions/ccd012-redemption-nyc.clar b/contracts/extensions/ccd012-redemption-nyc.clar index 12d5bf1..878e5c1 100644 --- a/contracts/extensions/ccd012-redemption-nyc.clar +++ b/contracts/extensions/ccd012-redemption-nyc.clar @@ -22,6 +22,7 @@ ;; helpers (define-constant MICRO_CITYCOINS (pow u10 u6)) ;; 6 decimal places +(define-constant REDEMPTION_SCALE_FACTOR (pow u10 u16)) ;; 16 decimal places ;; DATA VARS @@ -201,3 +202,13 @@ ) ;; PRIVATE FUNCTIONS + +;; CREDIT: ALEX math-fixed-point-16.clar + +(define-private (scale-up (a uint)) + (* a REDEMPTION_SCALE_FACTOR) +) + +(define-private (scale-down (a uint)) + (/ a REDEMPTION_SCALE_FACTOR) +) From 5b17a15977cb01045c0d01eef86191571983eb50 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 29 May 2024 10:59:43 -0700 Subject: [PATCH 41/65] fix: total supply should be >0 for both v1 and v2 Since this is calculated at initialization we need to make sure both supplies were correctly captured. --- contracts/extensions/ccd012-redemption-nyc.clar | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/extensions/ccd012-redemption-nyc.clar b/contracts/extensions/ccd012-redemption-nyc.clar index 878e5c1..43fe2c2 100644 --- a/contracts/extensions/ccd012-redemption-nyc.clar +++ b/contracts/extensions/ccd012-redemption-nyc.clar @@ -66,7 +66,7 @@ ;; check if sender is DAO or extension (try! (is-dao-or-extension)) ;; check that total supply is greater than 0 - (asserts! (or (> nycTotalSupplyV1 u0) (> nycTotalSupplyV2 u0)) ERR_GETTING_TOTAL_SUPPLY) + (asserts! (and (> nycTotalSupplyV1 u0) (> nycTotalSupplyV2 u0)) ERR_GETTING_TOTAL_SUPPLY) ;; check that redemption balance is greater than 0 (asserts! (> nycRedemptionBalance u0) ERR_GETTING_REDEMPTION_BALANCE) ;; check if redemptions are already enabled From 12d9582b1fde6799a95f16949871999c20adf786 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 29 May 2024 11:03:25 -0700 Subject: [PATCH 42/65] fix: move redemption ratio calculation to private fn --- contracts/extensions/ccd012-redemption-nyc.clar | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/contracts/extensions/ccd012-redemption-nyc.clar b/contracts/extensions/ccd012-redemption-nyc.clar index 43fe2c2..e3cb262 100644 --- a/contracts/extensions/ccd012-redemption-nyc.clar +++ b/contracts/extensions/ccd012-redemption-nyc.clar @@ -78,7 +78,7 @@ ;; record contract balance at block height (var-set contractBalance nycRedemptionBalance) ;; calculate redemption ratio - (var-set redemptionRatio (/ nycRedemptionBalance nycTotalSupply)) + (var-set redemptionRatio (calculate-redemption-ratio nycRedemptionBalance nycTotalSupply)) ;; set redemptionsEnabled to true, can only run once (var-set redemptionsEnabled true) ;; print redemption info @@ -212,3 +212,7 @@ (define-private (scale-down (a uint)) (/ a REDEMPTION_SCALE_FACTOR) ) + +(define-private (calculate-redemption-ratio (balance uint) (supply uint)) + u0 +) From 4e1861b9414bf1b1d1f54f62436b8999bf2f790d Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 29 May 2024 11:08:24 -0700 Subject: [PATCH 43/65] fix: add check for burned tokens Needs refinement, values are failing due to redemption ratio so can revisit once that's solved. --- tests/extensions/ccd012-redemption-nyc.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/extensions/ccd012-redemption-nyc.test.ts b/tests/extensions/ccd012-redemption-nyc.test.ts index d351c8b..e2875f4 100644 --- a/tests/extensions/ccd012-redemption-nyc.test.ts +++ b/tests/extensions/ccd012-redemption-nyc.test.ts @@ -283,6 +283,7 @@ Clarinet.test({ redeemBlock.receipts[i].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_BALANCE_NOT_FOUND); } else { redeemBlock.receipts[i].result.expectOk().expectUint(userInfoObjects[i - 1].redemptionAmount); + redeemBlock.events.expectFungibleTokenBurnEvent(0, user1.address, "newyorkcitycoin"); } } }, From 122ef6e26f28b653fdd2d061a84485cb17e6ada2 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 29 May 2024 18:56:25 -0700 Subject: [PATCH 44/65] fix: rework redemption ratio calculations Threw off some other tests, need to investigate. --- .../extensions/ccd012-redemption-nyc.clar | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/contracts/extensions/ccd012-redemption-nyc.clar b/contracts/extensions/ccd012-redemption-nyc.clar index e3cb262..f2c7189 100644 --- a/contracts/extensions/ccd012-redemption-nyc.clar +++ b/contracts/extensions/ccd012-redemption-nyc.clar @@ -174,9 +174,15 @@ ) (define-read-only (get-redemption-for-balance (balance uint)) - (begin - (asserts! (var-get redemptionsEnabled) none) - (some (* balance (var-get redemptionRatio))) + (let + ( + (balanceScaled (scale-up balance)) + (redemptionAmountScaled (* (var-get redemptionRatio) balanceScaled)) + ) + (if (> redemptionAmountScaled u0) + (some (scale-down redemptionAmountScaled)) + none + ) ) ) @@ -214,5 +220,11 @@ ) (define-private (calculate-redemption-ratio (balance uint) (supply uint)) - u0 + (let + ( + (balanceScaled (scale-up balance)) + (supplyScaled (scale-up supply)) + ) + (scale-down (/ balanceScaled supplyScaled)) + ) ) From fd0fc5d850bc2916f11f21a4c2d70b98d4f9c82b Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Thu, 30 May 2024 22:47:57 -0700 Subject: [PATCH 45/65] fix: modify redemption ratio calculation and usage --- .../extensions/ccd012-redemption-nyc.clar | 53 ++++++++++++++----- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/contracts/extensions/ccd012-redemption-nyc.clar b/contracts/extensions/ccd012-redemption-nyc.clar index f2c7189..d22f7d1 100644 --- a/contracts/extensions/ccd012-redemption-nyc.clar +++ b/contracts/extensions/ccd012-redemption-nyc.clar @@ -22,7 +22,7 @@ ;; helpers (define-constant MICRO_CITYCOINS (pow u10 u6)) ;; 6 decimal places -(define-constant REDEMPTION_SCALE_FACTOR (pow u10 u16)) ;; 16 decimal places +(define-constant REDEMPTION_SCALE_FACTOR (pow u10 u8)) ;; 8 decimal places ;; DATA VARS @@ -62,6 +62,7 @@ (nycTotalSupplyV2 (unwrap! (contract-call? .test-ccext-governance-token-nyc get-total-supply) ERR_PANIC)) (nycTotalSupply (+ (* nycTotalSupplyV1 MICRO_CITYCOINS) nycTotalSupplyV2)) (nycRedemptionBalance (as-contract (stx-get-balance tx-sender))) + (nycRedemptionRatio (unwrap! (calculate-redemption-ratio nycRedemptionBalance nycTotalSupply) ERR_PANIC)) ;; TODO: better error if we keep it ) ;; check if sender is DAO or extension (try! (is-dao-or-extension)) @@ -78,7 +79,7 @@ ;; record contract balance at block height (var-set contractBalance nycRedemptionBalance) ;; calculate redemption ratio - (var-set redemptionRatio (calculate-redemption-ratio nycRedemptionBalance nycTotalSupply)) + (var-set redemptionRatio nycRedemptionRatio) ;; set redemptionsEnabled to true, can only run once (var-set redemptionsEnabled true) ;; print redemption info @@ -176,11 +177,11 @@ (define-read-only (get-redemption-for-balance (balance uint)) (let ( - (balanceScaled (scale-up balance)) - (redemptionAmountScaled (* (var-get redemptionRatio) balanceScaled)) + (redemptionAmountScaled (* (var-get redemptionRatio) balance)) + (redemptionAmount (/ redemptionAmountScaled REDEMPTION_SCALE_FACTOR)) ) - (if (> redemptionAmountScaled u0) - (some (scale-down redemptionAmountScaled)) + (if (> redemptionAmount u0) + (some redemptionAmount) none ) ) @@ -215,16 +216,44 @@ (* a REDEMPTION_SCALE_FACTOR) ) +;; modified to favor the user when scaling down (define-private (scale-down (a uint)) - (/ a REDEMPTION_SCALE_FACTOR) + (let + ( + (quotient (/ a REDEMPTION_SCALE_FACTOR)) + (remainder (mod a REDEMPTION_SCALE_FACTOR)) + ) + (if (> remainder u0) + (+ quotient u1) + quotient + ) + ) ) +;; (define-private (calculate-redemption-ratio (balance uint) (supply uint)) +;; (let +;; ( +;; (balanceScaled (scale-up balance)) ;; u150000000000000000000000000000, +;; (supplyScaled (scale-up supply)) ;; u160200000000000000000000000000, +;; (quotient (if (> balanceScaled supplyScaled) (/ balanceScaled supplyScaled) u0)) ;; u0, because decimals are truncated +;; (remainder (mod balanceScaled supplyScaled)) ;; u150000000000000000000000000000, +;; ) +;; (if (and (> balanceScaled u0) (> supplyScaled u0)) +;; (some remainder) +;; none +;; ) +;; ) +;;) + (define-private (calculate-redemption-ratio (balance uint) (supply uint)) - (let - ( - (balanceScaled (scale-up balance)) - (supplyScaled (scale-up supply)) + (if (or (is-eq supply u0) (is-eq balance u0)) + none ;; If either supply or balance is zero, return 0 + (let + ( + (scaledBalance (* balance REDEMPTION_SCALE_FACTOR)) + (scaledSupply supply) + ) + (some (/ scaledBalance scaledSupply)) ;; Calculate the ratio ) - (scale-down (/ balanceScaled supplyScaled)) ) ) From 099de69b57cf12aaef12325837cf06c7d06fadeb Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Thu, 30 May 2024 23:18:36 -0700 Subject: [PATCH 46/65] fix: use optional for ratio calculation, update test formatting --- .../extensions/ccd012-redemption-nyc.clar | 34 ++++++------------- .../extensions/ccd012-redemption-nyc.test.ts | 23 +++++++------ 2 files changed, 23 insertions(+), 34 deletions(-) diff --git a/contracts/extensions/ccd012-redemption-nyc.clar b/contracts/extensions/ccd012-redemption-nyc.clar index d22f7d1..1f0434a 100644 --- a/contracts/extensions/ccd012-redemption-nyc.clar +++ b/contracts/extensions/ccd012-redemption-nyc.clar @@ -19,6 +19,7 @@ (define-constant ERR_BALANCE_NOT_FOUND (err u12006)) (define-constant ERR_NOTHING_TO_REDEEM (err u12007)) (define-constant ERR_ALREADY_CLAIMED (err u12008)) +(define-constant ERR_SUPPLY_CALCULATION (err u12009)) ;; helpers (define-constant MICRO_CITYCOINS (pow u10 u6)) ;; 6 decimal places @@ -62,7 +63,7 @@ (nycTotalSupplyV2 (unwrap! (contract-call? .test-ccext-governance-token-nyc get-total-supply) ERR_PANIC)) (nycTotalSupply (+ (* nycTotalSupplyV1 MICRO_CITYCOINS) nycTotalSupplyV2)) (nycRedemptionBalance (as-contract (stx-get-balance tx-sender))) - (nycRedemptionRatio (unwrap! (calculate-redemption-ratio nycRedemptionBalance nycTotalSupply) ERR_PANIC)) ;; TODO: better error if we keep it + (nycRedemptionRatio (calculate-redemption-ratio nycRedemptionBalance nycTotalSupply)) ) ;; check if sender is DAO or extension (try! (is-dao-or-extension)) @@ -70,6 +71,8 @@ (asserts! (and (> nycTotalSupplyV1 u0) (> nycTotalSupplyV2 u0)) ERR_GETTING_TOTAL_SUPPLY) ;; check that redemption balance is greater than 0 (asserts! (> nycRedemptionBalance u0) ERR_GETTING_REDEMPTION_BALANCE) + ;; check that redemption ratio has a value + (asserts! (is-some nycRedemptionRatio) ERR_SUPPLY_CALCULATION) ;; check if redemptions are already enabled (asserts! (not (var-get redemptionsEnabled)) ERR_ALREADY_ENABLED) ;; record current block height @@ -79,7 +82,7 @@ ;; record contract balance at block height (var-set contractBalance nycRedemptionBalance) ;; calculate redemption ratio - (var-set redemptionRatio nycRedemptionRatio) + (var-set redemptionRatio (unwrap-panic nycRedemptionRatio)) ;; set redemptionsEnabled to true, can only run once (var-set redemptionsEnabled true) ;; print redemption info @@ -96,7 +99,7 @@ (balanceV1 (unwrap! (contract-call? .test-ccext-governance-token-nyc-v1 get-balance userAddress) ERR_BALANCE_NOT_FOUND)) (balanceV2 (unwrap! (contract-call? .test-ccext-governance-token-nyc get-balance userAddress) ERR_BALANCE_NOT_FOUND)) (totalBalance (+ (* balanceV1 MICRO_CITYCOINS) balanceV2)) - (redemptionAmount (unwrap! (get-redemption-for-balance totalBalance) ERR_NOTHING_TO_REDEEM)) + (redemptionAmount (get-redemption-for-balance totalBalance)) (redemptionClaims (default-to u0 (get-redemption-amount-claimed userAddress))) ) ;; check if redemptions are enabled @@ -104,16 +107,16 @@ ;; check that user has at least one positive balance (asserts! (or (> balanceV1 u0) (> balanceV2 u0)) ERR_BALANCE_NOT_FOUND) ;; check that redemption amount is > 0 - (asserts! (> redemptionAmount u0) ERR_NOTHING_TO_REDEEM) + (asserts! (and (is-some redemptionAmount) (> (unwrap-panic redemptionAmount) u0)) ERR_NOTHING_TO_REDEEM) ;; burn NYC ;; MAINNET: SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token ;; MAINNET: SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 (and (> u0 balanceV1) (try! (contract-call? .test-ccext-governance-token-nyc-v1 burn balanceV1 userAddress))) (and (> u0 balanceV2) (try! (contract-call? .test-ccext-governance-token-nyc burn balanceV2 userAddress))) ;; transfer STX - (try! (as-contract (stx-transfer? redemptionAmount tx-sender userAddress))) + (try! (as-contract (stx-transfer? (unwrap-panic redemptionAmount) tx-sender userAddress))) ;; update redemption claims - (map-set RedemptionClaims userAddress (+ redemptionClaims redemptionAmount)) + (map-set RedemptionClaims userAddress (+ redemptionClaims (unwrap-panic redemptionAmount))) ;; print redemption info (print (get-redemption-info)) ;; print user redemption info @@ -230,30 +233,15 @@ ) ) -;; (define-private (calculate-redemption-ratio (balance uint) (supply uint)) -;; (let -;; ( -;; (balanceScaled (scale-up balance)) ;; u150000000000000000000000000000, -;; (supplyScaled (scale-up supply)) ;; u160200000000000000000000000000, -;; (quotient (if (> balanceScaled supplyScaled) (/ balanceScaled supplyScaled) u0)) ;; u0, because decimals are truncated -;; (remainder (mod balanceScaled supplyScaled)) ;; u150000000000000000000000000000, -;; ) -;; (if (and (> balanceScaled u0) (> supplyScaled u0)) -;; (some remainder) -;; none -;; ) -;; ) -;;) (define-private (calculate-redemption-ratio (balance uint) (supply uint)) (if (or (is-eq supply u0) (is-eq balance u0)) - none ;; If either supply or balance is zero, return 0 + none (let ( (scaledBalance (* balance REDEMPTION_SCALE_FACTOR)) - (scaledSupply supply) ) - (some (/ scaledBalance scaledSupply)) ;; Calculate the ratio + (some (/ scaledBalance supply)) ) ) ) diff --git a/tests/extensions/ccd012-redemption-nyc.test.ts b/tests/extensions/ccd012-redemption-nyc.test.ts index e2875f4..4defc9a 100644 --- a/tests/extensions/ccd012-redemption-nyc.test.ts +++ b/tests/extensions/ccd012-redemption-nyc.test.ts @@ -80,7 +80,6 @@ Clarinet.test({ const user2 = accounts.get("wallet_2")!; const user3 = accounts.get("wallet_3")!; const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); - const ccd012RedemptionNyc = new CCD012RedemptionNyc(chain, sender); const ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(chain, sender); // set stacking parameters @@ -122,11 +121,13 @@ Clarinet.test({ // assert assertEquals(executeBlock.receipts.length, 3); - executeBlock.receipts[0].result.expectOk().expectUint(1); - executeBlock.receipts[1].result.expectOk().expectUint(2); - executeBlock.receipts[2].result.expectOk().expectUint(3); + for (let i = 0; i < executeBlock.receipts.length; i++) { + executeBlock.receipts[i].result.expectOk().expectUint(i + 1); + } - const expectedEvent = `{blockHeight: u${executeBlock.height - 1}, contractBalance: u15000000000000, redemptionRatio: u0, redemptionsEnabled: true, totalSupply: u32020000000000}`; + const expectedEvent = `{blockHeight: u${executeBlock.height - 1}, contractBalance: u15000000000000, redemptionRatio: u46845721, redemptionsEnabled: true, totalSupply: u32020000000000}`; + // redemption ratio obtained through console logging below + // verified by the values: 15000000000000 / 32020000000000 = 0.46845721 // console.log(executeBlock.receipts[2].events[3].contract_event.value); executeBlock.receipts[2].events.expectPrintEvent(EXTENSIONS.CCD012_REDEMPTION_NYC, expectedEvent); }, @@ -257,7 +258,7 @@ Clarinet.test({ // get contract redemption info const redemptionInfo = await ccd012RedemptionNyc.getRedemptionInfo().result; - console.log("v1 only redemptionInfo", redemptionInfo); + console.log("v1 only redemptionInfo", parseClarityTuple(redemptionInfo)); // get user balances const user1Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user1.address).result; @@ -352,7 +353,7 @@ Clarinet.test({ // get contract redemption info const redemptionInfo = await ccd012RedemptionNyc.getRedemptionInfo().result; - console.log("v2 only redemptionInfo", redemptionInfo); + console.log("v2 only redemptionInfo", parseClarityTuple(redemptionInfo)); // get user balances const user1Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user1.address).result; @@ -369,7 +370,7 @@ Clarinet.test({ // act const redeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender), ccd012RedemptionNyc.redeemNyc(user1), ccd012RedemptionNyc.redeemNyc(user2), ccd012RedemptionNyc.redeemNyc(user3), ccd012RedemptionNyc.redeemNyc(user4)]); - console.log("v2 only redeem block", redeemBlock); + console.log("v2 only redeem block", parseClarityTuple(redeemBlock)); // assert assertEquals(redeemBlock.receipts.length, 5); @@ -448,7 +449,7 @@ Clarinet.test({ // get contract redemption info const redemptionInfo = await ccd012RedemptionNyc.getRedemptionInfo().result; - console.log("redemptionInfo", redemptionInfo); + console.log("v2 only redemptionInfo", parseClarityTuple(redemptionInfo)); // get user balances const user1Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user1.address).result; @@ -465,7 +466,7 @@ Clarinet.test({ // act const redeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender), ccd012RedemptionNyc.redeemNyc(user1), ccd012RedemptionNyc.redeemNyc(user2), ccd012RedemptionNyc.redeemNyc(user3), ccd012RedemptionNyc.redeemNyc(user4)]); - console.log("redeem block", redeemBlock); + console.log("v2 only redeem block", redeemBlock); // assert assertEquals(redeemBlock.receipts.length, 5); @@ -543,7 +544,7 @@ Clarinet.test({ // get contract redemption info const redemptionInfo = await ccd012RedemptionNyc.getRedemptionInfo().result; - console.log("multi redemptionInfo", redemptionInfo); + console.log("multi redemptionInfo", parseClarityTuple(redemptionInfo)); // get user balances const user1Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user1.address).result; From 8ce6d7222f9bae7468ba383795dd635a3b01bd06 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Fri, 31 May 2024 12:59:44 -0700 Subject: [PATCH 47/65] chore: update deps --- utils/deps.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/deps.ts b/utils/deps.ts index 70c5fc4..8d38186 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.7.1/index.ts"; +export type { Account, Block, ReadOnlyFn, TxReceipt } from "https://deno.land/x/clarinet@v1.8.0/index.ts"; -export { Clarinet, Chain, Tx, types } from "https://deno.land/x/clarinet@v1.7.1/index.ts"; +export { Clarinet, Chain, Tx, types } from "https://deno.land/x/clarinet@v1.8.0/index.ts"; export { assertEquals, assert } from "https://deno.land/std@0.201.0/testing/asserts.ts"; From c46bc8d26b60f78f926407fe558b9ec07791624d Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Fri, 31 May 2024 14:04:15 -0700 Subject: [PATCH 48/65] fix: update contract logic based on test results, fix tests for claims --- .../extensions/ccd012-redemption-nyc.clar | 8 +- .../extensions/ccd012-redemption-nyc.test.ts | 137 +++++++++++++++--- 2 files changed, 120 insertions(+), 25 deletions(-) diff --git a/contracts/extensions/ccd012-redemption-nyc.clar b/contracts/extensions/ccd012-redemption-nyc.clar index 1f0434a..88b1efc 100644 --- a/contracts/extensions/ccd012-redemption-nyc.clar +++ b/contracts/extensions/ccd012-redemption-nyc.clar @@ -68,7 +68,7 @@ ;; check if sender is DAO or extension (try! (is-dao-or-extension)) ;; check that total supply is greater than 0 - (asserts! (and (> nycTotalSupplyV1 u0) (> nycTotalSupplyV2 u0)) ERR_GETTING_TOTAL_SUPPLY) + (asserts! (> nycTotalSupply u0) ERR_GETTING_TOTAL_SUPPLY) ;; check that redemption balance is greater than 0 (asserts! (> nycRedemptionBalance u0) ERR_GETTING_REDEMPTION_BALANCE) ;; check that redemption ratio has a value @@ -105,14 +105,14 @@ ;; check if redemptions are enabled (asserts! (var-get redemptionsEnabled) ERR_NOT_ENABLED) ;; check that user has at least one positive balance - (asserts! (or (> balanceV1 u0) (> balanceV2 u0)) ERR_BALANCE_NOT_FOUND) + (asserts! (> (+ balanceV1 balanceV2) u0) ERR_BALANCE_NOT_FOUND) ;; cheaper, credit: LNow ;; check that redemption amount is > 0 (asserts! (and (is-some redemptionAmount) (> (unwrap-panic redemptionAmount) u0)) ERR_NOTHING_TO_REDEEM) ;; burn NYC ;; MAINNET: SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token ;; MAINNET: SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 - (and (> u0 balanceV1) (try! (contract-call? .test-ccext-governance-token-nyc-v1 burn balanceV1 userAddress))) - (and (> u0 balanceV2) (try! (contract-call? .test-ccext-governance-token-nyc burn balanceV2 userAddress))) + (and (> balanceV1 u0) (try! (contract-call? .test-ccext-governance-token-nyc-v1 burn balanceV1 userAddress))) + (and (> balanceV2 u0) (try! (contract-call? .test-ccext-governance-token-nyc burn balanceV2 userAddress))) ;; transfer STX (try! (as-contract (stx-transfer? (unwrap-panic redemptionAmount) tx-sender userAddress))) ;; update redemption claims diff --git a/tests/extensions/ccd012-redemption-nyc.test.ts b/tests/extensions/ccd012-redemption-nyc.test.ts index 4defc9a..db1b359 100644 --- a/tests/extensions/ccd012-redemption-nyc.test.ts +++ b/tests/extensions/ccd012-redemption-nyc.test.ts @@ -4,6 +4,10 @@ import { CCIP022TreasuryRedemptionNYC } from "../../models/proposals/ccip022-tre import { EXTENSIONS, PROPOSALS, constructAndPassProposal, nyc, parseClarityTuple, passProposal } from "../../utils/common.ts"; import { Account, assertEquals, Clarinet, Chain, types } from "../../utils/deps.ts"; +// used for asset identifier in detecting burn events +const NYC_V1_TOKEN = "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.test-ccext-governance-token-nyc-v1::newyorkcitycoin"; +const NYC_V2_TOKEN = "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.test-ccext-governance-token-nyc::newyorkcitycoin"; + // ============================= // 0. AUTHORIZATION CHECKS // ============================= @@ -98,7 +102,7 @@ Clarinet.test({ passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_003); // stack first cycle u1, last cycle u10 - const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked, lockPeriod)]); + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked, lockPeriod)]); // for length of mineBlock array, expectOk and expectBool(true) for (let i = 0; i < stackingBlock.receipts.length; i++) { stackingBlock.receipts[i].result.expectOk().expectBool(true); @@ -156,7 +160,10 @@ Clarinet.test({ // assert assertEquals(redeemBlock.receipts.length, 1); - redeemBlock.receipts[0].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_NOTHING_TO_REDEEM); + // note: actually fails with ERR_NOTHING_TO_REDEEM, but did not want to remove test case / extra code to be safe + // check was moved from let statement into function body with assert + // before being initialized the redemption amount always returns none, so short-circuits early before error + redeemBlock.receipts[0].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_NOT_ENABLED); }, }); @@ -180,9 +187,7 @@ Clarinet.test({ // assert assertEquals(redeemBlock.receipts.length, 1); - // note: actually fails with ERR_NOTHING_TO_REDEEM, but did not want to remove test case / extra code to be safe - // when disabled the redemption amount always returns none, so short-circuits early before error - redeemBlock.receipts[0].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_NOTHING_TO_REDEEM); + redeemBlock.receipts[0].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_NOT_ENABLED); }, }); @@ -283,10 +288,24 @@ Clarinet.test({ if (i === 0) { redeemBlock.receipts[i].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_BALANCE_NOT_FOUND); } else { - redeemBlock.receipts[i].result.expectOk().expectUint(userInfoObjects[i - 1].redemptionAmount); - redeemBlock.events.expectFungibleTokenBurnEvent(0, user1.address, "newyorkcitycoin"); + redeemBlock.receipts[i].result + .expectOk() + .expectSome() + .expectUint(userInfoObjects[i - 1].redemptionAmount); + const expectedBurnEvent = { + asset_identifier: NYC_V1_TOKEN, + sender: userInfoObjects[i - 1].address, + amount: userInfoObjects[i - 1].nycBalances.balanceV1, + }; + redeemBlock.receipts[i].events.expectFungibleTokenBurnEvent(expectedBurnEvent.amount, expectedBurnEvent.sender, expectedBurnEvent.asset_identifier); } } + console.log("----------"); + console.log("user1Info", user1InfoObject); + console.log("user2Info", user2InfoObject); + console.log("user3Info", user3InfoObject); + console.log("user4Info", user4InfoObject); + console.log("----------"); }, }); @@ -370,7 +389,7 @@ Clarinet.test({ // act const redeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender), ccd012RedemptionNyc.redeemNyc(user1), ccd012RedemptionNyc.redeemNyc(user2), ccd012RedemptionNyc.redeemNyc(user3), ccd012RedemptionNyc.redeemNyc(user4)]); - console.log("v2 only redeem block", parseClarityTuple(redeemBlock)); + console.log("v2 only redeem block", redeemBlock); // assert assertEquals(redeemBlock.receipts.length, 5); @@ -378,9 +397,24 @@ Clarinet.test({ if (i === 0) { redeemBlock.receipts[i].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_BALANCE_NOT_FOUND); } else { - redeemBlock.receipts[i].result.expectOk().expectUint(userInfoObjects[i - 1].redemptionAmount); + redeemBlock.receipts[i].result + .expectOk() + .expectSome() + .expectUint(userInfoObjects[i - 1].redemptionAmount); + const expectedBurnEvent = { + asset_identifier: NYC_V2_TOKEN, + sender: userInfoObjects[i - 1].address, + amount: userInfoObjects[i - 1].nycBalances.balanceV2, + }; + redeemBlock.receipts[i].events.expectFungibleTokenBurnEvent(expectedBurnEvent.amount, expectedBurnEvent.sender, expectedBurnEvent.asset_identifier); } } + console.log("----------"); + console.log("user1Info", user1InfoObject); + console.log("user2Info", user2InfoObject); + console.log("user3Info", user3InfoObject); + console.log("user4Info", user4InfoObject); + console.log("----------"); }, }); @@ -449,7 +483,7 @@ Clarinet.test({ // get contract redemption info const redemptionInfo = await ccd012RedemptionNyc.getRedemptionInfo().result; - console.log("v2 only redemptionInfo", parseClarityTuple(redemptionInfo)); + console.log("v1 + v2 redemptionInfo", parseClarityTuple(redemptionInfo)); // get user balances const user1Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user1.address).result; @@ -466,7 +500,7 @@ Clarinet.test({ // act const redeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender), ccd012RedemptionNyc.redeemNyc(user1), ccd012RedemptionNyc.redeemNyc(user2), ccd012RedemptionNyc.redeemNyc(user3), ccd012RedemptionNyc.redeemNyc(user4)]); - console.log("v2 only redeem block", redeemBlock); + console.log("v1 + v2 redeem block", redeemBlock); // assert assertEquals(redeemBlock.receipts.length, 5); @@ -474,9 +508,30 @@ Clarinet.test({ if (i === 0) { redeemBlock.receipts[i].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_BALANCE_NOT_FOUND); } else { - redeemBlock.receipts[i].result.expectOk().expectUint(userInfoObjects[i - 1].redemptionAmount); + redeemBlock.receipts[i].result + .expectOk() + .expectSome() + .expectUint(userInfoObjects[i - 1].redemptionAmount); + const expectedBurnEventV1 = { + asset_identifier: NYC_V1_TOKEN, + sender: userInfoObjects[i - 1].address, + amount: userInfoObjects[i - 1].nycBalances.balanceV1, + }; + const expectedBurnEventV2 = { + asset_identifier: NYC_V2_TOKEN, + sender: userInfoObjects[i - 1].address, + amount: userInfoObjects[i - 1].nycBalances.balanceV2, + }; + redeemBlock.receipts[i].events.expectFungibleTokenBurnEvent(expectedBurnEventV1.amount, expectedBurnEventV1.sender, expectedBurnEventV1.asset_identifier); + redeemBlock.receipts[i].events.expectFungibleTokenBurnEvent(expectedBurnEventV2.amount, expectedBurnEventV2.sender, expectedBurnEventV2.asset_identifier); } } + console.log("----------"); + console.log("user1Info", user1InfoObject); + console.log("user2Info", user2InfoObject); + console.log("user3Info", user3InfoObject); + console.log("user4Info", user4InfoObject); + console.log("----------"); }, }); @@ -518,7 +573,7 @@ Clarinet.test({ } // stack first cycle u1, last cycle u10 - const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked, lockPeriod)]); + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked * 2, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked * 3, lockPeriod), ccd007CityStacking.stack(user4, nyc.cityName, amountStacked * 4, lockPeriod)]); for (let i = 0; i < stackingBlock.receipts.length; i++) { stackingBlock.receipts[i].result.expectOk().expectBool(true); } @@ -567,21 +622,34 @@ Clarinet.test({ if (i === 0) { firstRedeemBlock.receipts[i].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_BALANCE_NOT_FOUND); } else { - firstRedeemBlock.receipts[i].result.expectOk().expectUint(userInfoObjects[i - 1].redemptionAmount); + firstRedeemBlock.receipts[i].result + .expectOk() + .expectSome() + .expectUint(userInfoObjects[i - 1].redemptionAmount); + const expectedBurnEventV1 = { + asset_identifier: NYC_V1_TOKEN, + sender: userInfoObjects[i - 1].address, + amount: userInfoObjects[i - 1].nycBalances.balanceV1, + }; + const expectedBurnEventV2 = { + asset_identifier: NYC_V2_TOKEN, + sender: userInfoObjects[i - 1].address, + amount: userInfoObjects[i - 1].nycBalances.balanceV2, + }; + firstRedeemBlock.receipts[i].events.expectFungibleTokenBurnEvent(expectedBurnEventV1.amount, expectedBurnEventV1.sender, expectedBurnEventV1.asset_identifier); + firstRedeemBlock.receipts[i].events.expectFungibleTokenBurnEvent(expectedBurnEventV2.amount, expectedBurnEventV2.sender, expectedBurnEventV2.asset_identifier); } } // claim stacking rewards from cycle u10 - const stackingClaimBlock = chain.mineBlock([ccd007CityStacking.claimStackingReward(user1, nyc.cityName, 10), ccd007CityStacking.claimStackingReward(user2, nyc.cityName, 10), ccd007CityStacking.claimStackingReward(user3, nyc.cityName, 10)]); + // progress the chain to cycle 15 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 16 + 10); + const stackingClaimBlock = chain.mineBlock([ccd007CityStacking.claimStackingReward(user1, nyc.cityName, 10), ccd007CityStacking.claimStackingReward(user2, nyc.cityName, 10), ccd007CityStacking.claimStackingReward(user3, nyc.cityName, 10), ccd007CityStacking.claimStackingReward(user4, nyc.cityName, 10)]); + // console.log("stackingClaimBlock", stackingClaimBlock); for (let i = 0; i < stackingClaimBlock.receipts.length; i++) { stackingClaimBlock.receipts[i].result.expectOk().expectBool(true); } - // act - // redeem token balances once for each user - const secondRedeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender), ccd012RedemptionNyc.redeemNyc(user1), ccd012RedemptionNyc.redeemNyc(user2), ccd012RedemptionNyc.redeemNyc(user3), ccd012RedemptionNyc.redeemNyc(user4)]); - console.log("secondRedeemBlock", secondRedeemBlock); - // get user balances const user1Info2 = await ccd012RedemptionNyc.getUserRedemptionInfo(user1.address).result; const user2Info2 = await ccd012RedemptionNyc.getUserRedemptionInfo(user2.address).result; @@ -595,14 +663,41 @@ Clarinet.test({ const userInfoObjects2 = [user1InfoObject2, user2InfoObject2, user3InfoObject2, user4InfoObject2]; + // act + // redeem token balances once for each user + const secondRedeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender), ccd012RedemptionNyc.redeemNyc(user1), ccd012RedemptionNyc.redeemNyc(user2), ccd012RedemptionNyc.redeemNyc(user3), ccd012RedemptionNyc.redeemNyc(user4)]); + console.log("secondRedeemBlock", secondRedeemBlock); + // assert assertEquals(secondRedeemBlock.receipts.length, 5); for (let i = 0; i < secondRedeemBlock.receipts.length; i++) { if (i === 0) { secondRedeemBlock.receipts[i].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_BALANCE_NOT_FOUND); } else { - secondRedeemBlock.receipts[i].result.expectOk().expectUint(userInfoObjects2[i - 1].redemptionAmount); + secondRedeemBlock.receipts[i].result + .expectOk() + .expectSome() + .expectUint(userInfoObjects2[i - 1].redemptionAmount); + const expectedBurnEventV1 = { + asset_identifier: NYC_V1_TOKEN, + sender: userInfoObjects[i - 1].address, + amount: userInfoObjects[i - 1].nycBalances.balanceV1, + }; + const expectedBurnEventV2 = { + asset_identifier: NYC_V2_TOKEN, + sender: userInfoObjects[i - 1].address, + amount: userInfoObjects[i - 1].nycBalances.balanceV2, + }; + firstRedeemBlock.receipts[i].events.expectFungibleTokenBurnEvent(expectedBurnEventV1.amount, expectedBurnEventV1.sender, expectedBurnEventV1.asset_identifier); + firstRedeemBlock.receipts[i].events.expectFungibleTokenBurnEvent(expectedBurnEventV2.amount, expectedBurnEventV2.sender, expectedBurnEventV2.asset_identifier); } } + + console.log("----------"); + console.log("user1Info", user1InfoObject2); + console.log("user2Info", user2InfoObject2); + console.log("user3Info", user3InfoObject2); + console.log("user4Info", user4InfoObject2); + console.log("----------"); }, }); From f392ced2bb987e8cd099902557a50d2597f69194 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Fri, 31 May 2024 14:06:05 -0700 Subject: [PATCH 49/65] chore: cleanup console logging, check all tests pass --- .../extensions/ccd012-redemption-nyc.test.ts | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/tests/extensions/ccd012-redemption-nyc.test.ts b/tests/extensions/ccd012-redemption-nyc.test.ts index db1b359..3c2ec75 100644 --- a/tests/extensions/ccd012-redemption-nyc.test.ts +++ b/tests/extensions/ccd012-redemption-nyc.test.ts @@ -263,7 +263,7 @@ Clarinet.test({ // get contract redemption info const redemptionInfo = await ccd012RedemptionNyc.getRedemptionInfo().result; - console.log("v1 only redemptionInfo", parseClarityTuple(redemptionInfo)); + // console.log("v1 only redemptionInfo", parseClarityTuple(redemptionInfo)); // get user balances const user1Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user1.address).result; @@ -280,7 +280,7 @@ Clarinet.test({ // act const redeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender), ccd012RedemptionNyc.redeemNyc(user1), ccd012RedemptionNyc.redeemNyc(user2), ccd012RedemptionNyc.redeemNyc(user3), ccd012RedemptionNyc.redeemNyc(user4)]); - console.log("v1 only redeem block", redeemBlock); + // console.log("v1 only redeem block", redeemBlock); // assert assertEquals(redeemBlock.receipts.length, 5); @@ -300,12 +300,12 @@ Clarinet.test({ redeemBlock.receipts[i].events.expectFungibleTokenBurnEvent(expectedBurnEvent.amount, expectedBurnEvent.sender, expectedBurnEvent.asset_identifier); } } - console.log("----------"); - console.log("user1Info", user1InfoObject); - console.log("user2Info", user2InfoObject); - console.log("user3Info", user3InfoObject); - console.log("user4Info", user4InfoObject); - console.log("----------"); + // console.log("----------"); + // console.log("user1Info", user1InfoObject); + // console.log("user2Info", user2InfoObject); + // console.log("user3Info", user3InfoObject); + // console.log("user4Info", user4InfoObject); + // console.log("----------"); }, }); @@ -372,7 +372,7 @@ Clarinet.test({ // get contract redemption info const redemptionInfo = await ccd012RedemptionNyc.getRedemptionInfo().result; - console.log("v2 only redemptionInfo", parseClarityTuple(redemptionInfo)); + // console.log("v2 only redemptionInfo", parseClarityTuple(redemptionInfo)); // get user balances const user1Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user1.address).result; @@ -389,7 +389,7 @@ Clarinet.test({ // act const redeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender), ccd012RedemptionNyc.redeemNyc(user1), ccd012RedemptionNyc.redeemNyc(user2), ccd012RedemptionNyc.redeemNyc(user3), ccd012RedemptionNyc.redeemNyc(user4)]); - console.log("v2 only redeem block", redeemBlock); + // console.log("v2 only redeem block", redeemBlock); // assert assertEquals(redeemBlock.receipts.length, 5); @@ -409,12 +409,12 @@ Clarinet.test({ redeemBlock.receipts[i].events.expectFungibleTokenBurnEvent(expectedBurnEvent.amount, expectedBurnEvent.sender, expectedBurnEvent.asset_identifier); } } - console.log("----------"); - console.log("user1Info", user1InfoObject); - console.log("user2Info", user2InfoObject); - console.log("user3Info", user3InfoObject); - console.log("user4Info", user4InfoObject); - console.log("----------"); + // console.log("----------"); + // console.log("user1Info", user1InfoObject); + // console.log("user2Info", user2InfoObject); + // console.log("user3Info", user3InfoObject); + // console.log("user4Info", user4InfoObject); + // console.log("----------"); }, }); @@ -483,7 +483,7 @@ Clarinet.test({ // get contract redemption info const redemptionInfo = await ccd012RedemptionNyc.getRedemptionInfo().result; - console.log("v1 + v2 redemptionInfo", parseClarityTuple(redemptionInfo)); + // console.log("v1 + v2 redemptionInfo", parseClarityTuple(redemptionInfo)); // get user balances const user1Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user1.address).result; @@ -500,7 +500,7 @@ Clarinet.test({ // act const redeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender), ccd012RedemptionNyc.redeemNyc(user1), ccd012RedemptionNyc.redeemNyc(user2), ccd012RedemptionNyc.redeemNyc(user3), ccd012RedemptionNyc.redeemNyc(user4)]); - console.log("v1 + v2 redeem block", redeemBlock); + // console.log("v1 + v2 redeem block", redeemBlock); // assert assertEquals(redeemBlock.receipts.length, 5); @@ -526,12 +526,12 @@ Clarinet.test({ redeemBlock.receipts[i].events.expectFungibleTokenBurnEvent(expectedBurnEventV2.amount, expectedBurnEventV2.sender, expectedBurnEventV2.asset_identifier); } } - console.log("----------"); - console.log("user1Info", user1InfoObject); - console.log("user2Info", user2InfoObject); - console.log("user3Info", user3InfoObject); - console.log("user4Info", user4InfoObject); - console.log("----------"); + // console.log("----------"); + // console.log("user1Info", user1InfoObject); + // console.log("user2Info", user2InfoObject); + // console.log("user3Info", user3InfoObject); + // console.log("user4Info", user4InfoObject); + // console.log("----------"); }, }); @@ -599,7 +599,7 @@ Clarinet.test({ // get contract redemption info const redemptionInfo = await ccd012RedemptionNyc.getRedemptionInfo().result; - console.log("multi redemptionInfo", parseClarityTuple(redemptionInfo)); + // console.log("multi redemptionInfo", parseClarityTuple(redemptionInfo)); // get user balances const user1Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user1.address).result; @@ -616,7 +616,7 @@ Clarinet.test({ // redeem token balances once for each user const firstRedeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender), ccd012RedemptionNyc.redeemNyc(user1), ccd012RedemptionNyc.redeemNyc(user2), ccd012RedemptionNyc.redeemNyc(user3), ccd012RedemptionNyc.redeemNyc(user4)]); - console.log("firstRedeemBlock", firstRedeemBlock); + // console.log("firstRedeemBlock", firstRedeemBlock); assertEquals(firstRedeemBlock.receipts.length, 5); for (let i = 0; i < firstRedeemBlock.receipts.length; i++) { if (i === 0) { @@ -666,7 +666,7 @@ Clarinet.test({ // act // redeem token balances once for each user const secondRedeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender), ccd012RedemptionNyc.redeemNyc(user1), ccd012RedemptionNyc.redeemNyc(user2), ccd012RedemptionNyc.redeemNyc(user3), ccd012RedemptionNyc.redeemNyc(user4)]); - console.log("secondRedeemBlock", secondRedeemBlock); + // console.log("secondRedeemBlock", secondRedeemBlock); // assert assertEquals(secondRedeemBlock.receipts.length, 5); @@ -693,11 +693,11 @@ Clarinet.test({ } } - console.log("----------"); - console.log("user1Info", user1InfoObject2); - console.log("user2Info", user2InfoObject2); - console.log("user3Info", user3InfoObject2); - console.log("user4Info", user4InfoObject2); - console.log("----------"); + // console.log("----------"); + // console.log("user1Info", user1InfoObject2); + // console.log("user2Info", user2InfoObject2); + // console.log("user3Info", user3InfoObject2); + // console.log("user4Info", user4InfoObject2); + // console.log("----------"); }, }); From 349745091e2b345a947770e02a011cc6b9ef82de Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Tue, 4 Jun 2024 19:04:28 -0700 Subject: [PATCH 50/65] fix: add totalRedeemed var for amount transferred from contract --- contracts/extensions/ccd012-redemption-nyc.clar | 9 ++++++++- tests/extensions/ccd012-redemption-nyc.test.ts | 5 ++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/contracts/extensions/ccd012-redemption-nyc.clar b/contracts/extensions/ccd012-redemption-nyc.clar index 88b1efc..15b611c 100644 --- a/contracts/extensions/ccd012-redemption-nyc.clar +++ b/contracts/extensions/ccd012-redemption-nyc.clar @@ -32,6 +32,7 @@ (define-data-var totalSupply uint u0) (define-data-var contractBalance uint u0) (define-data-var redemptionRatio uint u0) +(define-data-var totalRedeemed uint u0) ;; DATA MAPS @@ -116,6 +117,7 @@ ;; transfer STX (try! (as-contract (stx-transfer? (unwrap-panic redemptionAmount) tx-sender userAddress))) ;; update redemption claims + (var-set totalRedeemed (+ (var-get totalRedeemed) (unwrap-panic redemptionAmount))) (map-set RedemptionClaims userAddress (+ redemptionClaims (unwrap-panic redemptionAmount))) ;; print redemption info (print (get-redemption-info)) @@ -148,6 +150,10 @@ (var-get redemptionRatio) ) +(define-read-only (get-total-redeemed) + (var-get totalRedeemed) +) + ;; aggregate all exposed vars above (define-read-only (get-redemption-info) { @@ -155,7 +161,8 @@ blockHeight: (get-redemption-block-height), totalSupply: (get-redemption-total-supply), contractBalance: (get-redemption-contract-balance), - redemptionRatio: (get-redemption-ratio) + redemptionRatio: (get-redemption-ratio), + totalRedeemed: (get-total-redeemed) } ) diff --git a/tests/extensions/ccd012-redemption-nyc.test.ts b/tests/extensions/ccd012-redemption-nyc.test.ts index 3c2ec75..a917d68 100644 --- a/tests/extensions/ccd012-redemption-nyc.test.ts +++ b/tests/extensions/ccd012-redemption-nyc.test.ts @@ -129,7 +129,7 @@ Clarinet.test({ executeBlock.receipts[i].result.expectOk().expectUint(i + 1); } - const expectedEvent = `{blockHeight: u${executeBlock.height - 1}, contractBalance: u15000000000000, redemptionRatio: u46845721, redemptionsEnabled: true, totalSupply: u32020000000000}`; + const expectedEvent = `{blockHeight: u${executeBlock.height - 1}, contractBalance: u15000000000000, redemptionRatio: u46845721, redemptionsEnabled: true, totalRedeemed: u0, totalSupply: u32020000000000}`; // redemption ratio obtained through console logging below // verified by the values: 15000000000000 / 32020000000000 = 0.46845721 // console.log(executeBlock.receipts[2].events[3].contract_event.value); @@ -693,6 +693,9 @@ Clarinet.test({ } } + // console.log("----------"); + // console.log(await ccd012RedemptionNyc.getRedemptionInfo().result); + // console.log("----------"); // console.log("user1Info", user1InfoObject2); // console.log("user2Info", user2InfoObject2); From a805e9a1c0925d53241d4d5e0c4719f757e6cdb2 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Tue, 4 Jun 2024 19:18:50 -0700 Subject: [PATCH 51/65] fix: update print format per Clarity WG discussion We should match SIP-019 wherever possible in print formats, tagging @setzeus @rafaelcr URL: https://github.com/stacksgov/sips/blob/main/sips/sip-019/sip-019-token-metadata-update-notifications.md#notification-structure-reusability --- contracts/extensions/ccd012-redemption-nyc.clar | 15 ++++++++++++--- tests/extensions/ccd012-redemption-nyc.test.ts | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/contracts/extensions/ccd012-redemption-nyc.clar b/contracts/extensions/ccd012-redemption-nyc.clar index 15b611c..e339920 100644 --- a/contracts/extensions/ccd012-redemption-nyc.clar +++ b/contracts/extensions/ccd012-redemption-nyc.clar @@ -87,7 +87,10 @@ ;; set redemptionsEnabled to true, can only run once (var-set redemptionsEnabled true) ;; print redemption info - (ok (print (get-redemption-info))) + (ok (print { + notification: "intialize-contract", + payload: (get-redemption-info) + })) ) ) @@ -120,9 +123,15 @@ (var-set totalRedeemed (+ (var-get totalRedeemed) (unwrap-panic redemptionAmount))) (map-set RedemptionClaims userAddress (+ redemptionClaims (unwrap-panic redemptionAmount))) ;; print redemption info - (print (get-redemption-info)) + (print { + notification: "contract-redemption", + payload: (get-redemption-info) + }) ;; print user redemption info - (print (try! (get-user-redemption-info userAddress))) + (print { + notification: "user-redemption", + payload: (try! (get-user-redemption-info userAddress)) + }) ;; return redemption amount (ok redemptionAmount) ) diff --git a/tests/extensions/ccd012-redemption-nyc.test.ts b/tests/extensions/ccd012-redemption-nyc.test.ts index a917d68..1cab303 100644 --- a/tests/extensions/ccd012-redemption-nyc.test.ts +++ b/tests/extensions/ccd012-redemption-nyc.test.ts @@ -129,7 +129,7 @@ Clarinet.test({ executeBlock.receipts[i].result.expectOk().expectUint(i + 1); } - const expectedEvent = `{blockHeight: u${executeBlock.height - 1}, contractBalance: u15000000000000, redemptionRatio: u46845721, redemptionsEnabled: true, totalRedeemed: u0, totalSupply: u32020000000000}`; + const expectedEvent = `{notification: "intialize-contract", payload: {blockHeight: u12611, contractBalance: u15000000000000, redemptionRatio: u46845721, redemptionsEnabled: true, totalRedeemed: u0, totalSupply: u32020000000000}}`; // redemption ratio obtained through console logging below // verified by the values: 15000000000000 / 32020000000000 = 0.46845721 // console.log(executeBlock.receipts[2].events[3].contract_event.value); From 6d49347d3177f0e53c1bd8c0b36572d75e496978 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Tue, 4 Jun 2024 19:56:38 -0700 Subject: [PATCH 52/65] chore: separate legacy and regular testing jobs Trying to isolate a failure with the latest Clarinet, likely a non-issue but this will allow both old 1.8.0 and new 2.6.0 to run in parallel. --- ...st-contracts.yaml => clarinet-legacy.yaml} | 9 +----- .github/workflows/clarinet.yaml | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+), 8 deletions(-) rename .github/workflows/{test-contracts.yaml => clarinet-legacy.yaml} (75%) create mode 100644 .github/workflows/clarinet.yaml diff --git a/.github/workflows/test-contracts.yaml b/.github/workflows/clarinet-legacy.yaml similarity index 75% rename from .github/workflows/test-contracts.yaml rename to .github/workflows/clarinet-legacy.yaml index de67fbd..53bffbe 100644 --- a/.github/workflows/test-contracts.yaml +++ b/.github/workflows/clarinet-legacy.yaml @@ -1,7 +1,4 @@ -# GitHub action to run Clarinet tests against the contracts -# and fail if code is invalid or a test fails. - -name: Test Contracts +name: Test contracts with clarinet (legacy) on: push: @@ -24,10 +21,6 @@ jobs: steps: - name: "Checkout code" uses: actions/checkout@v4 - - name: "Check contract syntax" - uses: docker://hirosystems/clarinet:2.6.0 - with: - args: check - name: "Check contract syntax (legacy)" uses: docker://hirosystems/clarinet:1.8.0 with: diff --git a/.github/workflows/clarinet.yaml b/.github/workflows/clarinet.yaml new file mode 100644 index 0000000..e761252 --- /dev/null +++ b/.github/workflows/clarinet.yaml @@ -0,0 +1,28 @@ +name: Test contracts with clarinet + +on: + push: + branches: + - main + - develop + pull_request: + branches: + - main + - develop + # allow running manually + workflow_dispatch: + +jobs: + test-contracts: + runs-on: ubuntu-latest + steps: + - name: "Checkout code" + uses: actions/checkout@v4 + - name: "Configure clarinet" + run: | + mkdir -p ~/.clarinet + echo "enable_hints = false" >> ~/.clarinet/clarinetrc.toml + - name: "Check contract syntax" + uses: docker://hirosystems/clarinet:2.6.0 + with: + args: check From ec4c0f83bad8584a4afac8f28a7b0504fc329465 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 12 Jun 2024 06:26:11 -0700 Subject: [PATCH 53/65] fix: add test to verify redemption ratio and contract balance after claims --- .../extensions/ccd012-redemption-nyc.test.ts | 28 ++++++++++++++++--- utils/deps.ts | 2 +- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/tests/extensions/ccd012-redemption-nyc.test.ts b/tests/extensions/ccd012-redemption-nyc.test.ts index 1cab303..6ef9030 100644 --- a/tests/extensions/ccd012-redemption-nyc.test.ts +++ b/tests/extensions/ccd012-redemption-nyc.test.ts @@ -2,7 +2,7 @@ import { CCD007CityStacking } from "../../models/extensions/ccd007-citycoin-stac import { CCD012RedemptionNyc } from "../../models/extensions/ccd012-redemption-nyc.model.ts"; import { CCIP022TreasuryRedemptionNYC } from "../../models/proposals/ccip022-treasury-redemption-nyc.model.ts"; import { EXTENSIONS, PROPOSALS, constructAndPassProposal, nyc, parseClarityTuple, passProposal } from "../../utils/common.ts"; -import { Account, assertEquals, Clarinet, Chain, types } from "../../utils/deps.ts"; +import { Account, Clarinet, Chain, assertEquals, assertAlmostEquals } from "../../utils/deps.ts"; // used for asset identifier in detecting burn events const NYC_V1_TOKEN = "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.test-ccext-governance-token-nyc-v1::newyorkcitycoin"; @@ -129,9 +129,10 @@ Clarinet.test({ executeBlock.receipts[i].result.expectOk().expectUint(i + 1); } - const expectedEvent = `{notification: "intialize-contract", payload: {blockHeight: u12611, contractBalance: u15000000000000, redemptionRatio: u46845721, redemptionsEnabled: true, totalRedeemed: u0, totalSupply: u32020000000000}}`; + const expectedEvent = `{notification: "intialize-contract", payload: {blockHeight: u12611, contractBalance: u15000000000000, currentContractBalance: u15000000000000, redemptionRatio: u46845721, redemptionsEnabled: true, totalRedeemed: u0, totalSupply: u32020000000000}}`; // redemption ratio obtained through console logging below // verified by the values: 15000000000000 / 32020000000000 = 0.46845721 + // console.log(expectedEvent); // console.log(executeBlock.receipts[2].events[3].contract_event.value); executeBlock.receipts[2].events.expectPrintEvent(EXTENSIONS.CCD012_REDEMPTION_NYC, expectedEvent); }, @@ -693,8 +694,27 @@ Clarinet.test({ } } - // console.log("----------"); - // console.log(await ccd012RedemptionNyc.getRedemptionInfo().result); + // get the redemption info from the contract for analysis + const redemptionInfoResult = parseClarityTuple(await ccd012RedemptionNyc.getRedemptionInfo().result); + + // console.log("------------------------------"); + // console.log("get redemption info"); + // console.log(redemptionInfoResult); + + // calculate the redemption ratios for comparison + const redemptionDecimals = 8; + const redemptionScaleFactor = 10 ** redemptionDecimals; + const redemptionRatio1 = redemptionInfoResult.redemptionRatio / redemptionScaleFactor; + const redemptionRatio2 = redemptionInfoResult.contractBalance / redemptionInfoResult.totalSupply; + + // console.log("redemption ratio 1: ", redemptionRatio1); + // console.log("redemption ratio 2: ", redemptionRatio2); + + // check that the ratio is correctly set based on known balance and total supply + assertAlmostEquals(redemptionRatio1, redemptionRatio2, redemptionDecimals); + + // check that the balance is equal to first known balance minus redeemed amount + assertEquals(Number(redemptionInfoResult.currentContractBalance), redemptionInfoResult.contractBalance - redemptionInfoResult.totalRedeemed); // console.log("----------"); // console.log("user1Info", user1InfoObject2); diff --git a/utils/deps.ts b/utils/deps.ts index 8d38186..5985f48 100644 --- a/utils/deps.ts +++ b/utils/deps.ts @@ -2,4 +2,4 @@ export type { Account, Block, ReadOnlyFn, TxReceipt } from "https://deno.land/x/ export { Clarinet, Chain, Tx, types } from "https://deno.land/x/clarinet@v1.8.0/index.ts"; -export { assertEquals, assert } from "https://deno.land/std@0.201.0/testing/asserts.ts"; +export { assertAlmostEquals, assertEquals, assert } from "https://deno.land/std@0.201.0/testing/asserts.ts"; From 41ef321b62dc2054d7a4ceab3b43229528886221 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 12 Jun 2024 06:28:47 -0700 Subject: [PATCH 54/65] fix: clean up and consolidate functions Adds fn to get the current balance of contract and returns that with aggregated info call. Adds fn to check the contract balance is equal to original balance minus redemptions after claims. --- contracts/extensions/ccd012-redemption-nyc.clar | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/contracts/extensions/ccd012-redemption-nyc.clar b/contracts/extensions/ccd012-redemption-nyc.clar index e339920..1df5ebe 100644 --- a/contracts/extensions/ccd012-redemption-nyc.clar +++ b/contracts/extensions/ccd012-redemption-nyc.clar @@ -22,6 +22,7 @@ (define-constant ERR_SUPPLY_CALCULATION (err u12009)) ;; helpers +(define-constant SELF (as-contract tx-sender)) (define-constant MICRO_CITYCOINS (pow u10 u6)) ;; 6 decimal places (define-constant REDEMPTION_SCALE_FACTOR (pow u10 u8)) ;; 8 decimal places @@ -63,7 +64,7 @@ (nycTotalSupplyV1 (unwrap! (contract-call? .test-ccext-governance-token-nyc-v1 get-total-supply) ERR_PANIC)) (nycTotalSupplyV2 (unwrap! (contract-call? .test-ccext-governance-token-nyc get-total-supply) ERR_PANIC)) (nycTotalSupply (+ (* nycTotalSupplyV1 MICRO_CITYCOINS) nycTotalSupplyV2)) - (nycRedemptionBalance (as-contract (stx-get-balance tx-sender))) + (nycRedemptionBalance (get-redemption-contract-current-balance)) (nycRedemptionRatio (calculate-redemption-ratio nycRedemptionBalance nycTotalSupply)) ) ;; check if sender is DAO or extension @@ -155,6 +156,10 @@ (var-get contractBalance) ) +(define-read-only (get-redemption-contract-current-balance) + (stx-get-balance SELF) +) + (define-read-only (get-redemption-ratio) (var-get redemptionRatio) ) @@ -170,6 +175,7 @@ blockHeight: (get-redemption-block-height), totalSupply: (get-redemption-total-supply), contractBalance: (get-redemption-contract-balance), + currentContractBalance: (get-redemption-contract-current-balance), redemptionRatio: (get-redemption-ratio), totalRedeemed: (get-total-redeemed) } From 1135d9ce14341ac78107900b22e8926eab827e68 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 12 Jun 2024 06:37:58 -0700 Subject: [PATCH 55/65] fix: upload cache of requirements files for testing structure This uploads the contracts defined in the requirements section of Clarinet.toml, which are no longer available on testnet due to the reset. --- .../SP000000000000000000002Q6VF78.pox.clar | 695 ++++++++++++ .../SP000000000000000000002Q6VF78.pox.json | 4 + ...YGEZT0JDGEB8Y634C7R.miamicoin-auth-v2.clar | 630 +++++++++++ ...YGEZT0JDGEB8Y634C7R.miamicoin-auth-v2.json | 4 + ...JDGEB8Y634C7R.miamicoin-core-v1-patch.clar | 67 ++ ...JDGEB8Y634C7R.miamicoin-core-v1-patch.json | 4 + ...YGEZT0JDGEB8Y634C7R.miamicoin-core-v2.clar | 1008 +++++++++++++++++ ...YGEZT0JDGEB8Y634C7R.miamicoin-core-v2.json | 4 + ...GEZT0JDGEB8Y634C7R.miamicoin-token-v2.clar | 301 +++++ ...GEZT0JDGEB8Y634C7R.miamicoin-token-v2.json | 4 + ...QN17ETGQS3527SA5.newyorkcitycoin-auth.clar | 564 +++++++++ ...QN17ETGQS3527SA5.newyorkcitycoin-auth.json | 4 + ...N17ETGQS3527SA5.newyorkcitycoin-token.clar | 177 +++ ...N17ETGQS3527SA5.newyorkcitycoin-token.json | 4 + ...NFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.clar | 15 + ...NFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.json | 4 + ...7RX8QJ5SVTE.sip-010-trait-ft-standard.clar | 24 + ...7RX8QJ5SVTE.sip-010-trait-ft-standard.json | 4 + ...QRZN1MYEDTAR0KP27.citycoin-core-trait.clar | 35 + ...QRZN1MYEDTAR0KP27.citycoin-core-trait.json | 4 + ...RZN1MYEDTAR0KP27.citycoin-token-trait.clar | 27 + ...RZN1MYEDTAR0KP27.citycoin-token-trait.json | 4 + ...9T199QRZN1MYEDTAR0KP27.miamicoin-auth.clar | 563 +++++++++ ...9T199QRZN1MYEDTAR0KP27.miamicoin-auth.json | 4 + ...T199QRZN1MYEDTAR0KP27.miamicoin-token.clar | 179 +++ ...T199QRZN1MYEDTAR0KP27.miamicoin-token.json | 4 + ...FEDJ9R1F4DYQ11.citycoin-core-v2-trait.clar | 47 + ...FEDJ9R1F4DYQ11.citycoin-core-v2-trait.json | 4 + ...EDJ9R1F4DYQ11.citycoin-token-v2-trait.clar | 35 + ...EDJ9R1F4DYQ11.citycoin-token-v2-trait.json | 4 + ...84YE1NQFEDJ9R1F4DYQ11.citycoin-vrf-v2.clar | 69 ++ ...84YE1NQFEDJ9R1F4DYQ11.citycoin-vrf-v2.json | 4 + ...EDJ9R1F4DYQ11.newyorkcitycoin-auth-v2.clar | 630 +++++++++++ ...EDJ9R1F4DYQ11.newyorkcitycoin-auth-v2.json | 4 + ...EDJ9R1F4DYQ11.newyorkcitycoin-core-v2.clar | 1008 +++++++++++++++++ ...EDJ9R1F4DYQ11.newyorkcitycoin-core-v2.json | 4 + ...DJ9R1F4DYQ11.newyorkcitycoin-token-v2.clar | 301 +++++ ...DJ9R1F4DYQ11.newyorkcitycoin-token-v2.json | 4 + .../ST000000000000000000002AMW42H.pox.clar | 695 ++++++++++++ .../ST000000000000000000002AMW42H.pox.json | 4 + ...YGEZT0JDGEB8WRH7C6H.miamicoin-auth-v2.clar | 630 +++++++++++ ...YGEZT0JDGEB8WRH7C6H.miamicoin-auth-v2.json | 4 + ...YGEZT0JDGEB8WRH7C6H.miamicoin-core-v2.clar | 1007 ++++++++++++++++ ...YGEZT0JDGEB8WRH7C6H.miamicoin-core-v2.json | 4 + ...GEZT0JDGEB8WRH7C6H.miamicoin-token-v2.clar | 281 +++++ ...GEZT0JDGEB8WRH7C6H.miamicoin-token-v2.json | 4 + ...6FD41MVNP3JS1GABZ8TRVX023PT.nft-trait.clar | 15 + ...6FD41MVNP3JS1GABZ8TRVX023PT.nft-trait.json | 4 + ...Z8TRVX023PT.sip-010-trait-ft-standard.clar | 24 + ...Z8TRVX023PT.sip-010-trait-ft-standard.json | 4 + ...7SHM6KPA085ES6.citycoin-core-v2-trait.clar | 47 + ...7SHM6KPA085ES6.citycoin-core-v2-trait.json | 4 + ...SHM6KPA085ES6.citycoin-token-v2-trait.clar | 35 + ...SHM6KPA085ES6.citycoin-token-v2-trait.json | 4 + ...3HCHPEY7SHM6KPA085ES6.citycoin-vrf-v2.clar | 69 ++ ...3HCHPEY7SHM6KPA085ES6.citycoin-vrf-v2.json | 4 + ...EDJ9R1D64KKHQ.newyorkcitycoin-auth-v2.clar | 630 +++++++++++ ...EDJ9R1D64KKHQ.newyorkcitycoin-auth-v2.json | 4 + ...EDJ9R1D64KKHQ.newyorkcitycoin-core-v2.clar | 1007 ++++++++++++++++ ...EDJ9R1D64KKHQ.newyorkcitycoin-core-v2.json | 4 + ...DJ9R1D64KKHQ.newyorkcitycoin-token-v2.clar | 281 +++++ ...DJ9R1D64KKHQ.newyorkcitycoin-token-v2.json | 4 + .gitignore | 12 +- 63 files changed, 11230 insertions(+), 2 deletions(-) create mode 100644 .cache/requirements/SP000000000000000000002Q6VF78.pox.clar create mode 100644 .cache/requirements/SP000000000000000000002Q6VF78.pox.json create mode 100644 .cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-auth-v2.clar create mode 100644 .cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-auth-v2.json create mode 100644 .cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v1-patch.clar create mode 100644 .cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v1-patch.json create mode 100644 .cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v2.clar create mode 100644 .cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v2.json create mode 100644 .cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2.clar create mode 100644 .cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2.json create mode 100644 .cache/requirements/SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-auth.clar create mode 100644 .cache/requirements/SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-auth.json create mode 100644 .cache/requirements/SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token.clar create mode 100644 .cache/requirements/SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token.json create mode 100644 .cache/requirements/SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.clar create mode 100644 .cache/requirements/SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.json create mode 100644 .cache/requirements/SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.clar create mode 100644 .cache/requirements/SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.json create mode 100644 .cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.clar create mode 100644 .cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.json create mode 100644 .cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-token-trait.clar create mode 100644 .cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-token-trait.json create mode 100644 .cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-auth.clar create mode 100644 .cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-auth.json create mode 100644 .cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-token.clar create mode 100644 .cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-token.json create mode 100644 .cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-core-v2-trait.clar create mode 100644 .cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-core-v2-trait.json create mode 100644 .cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-token-v2-trait.clar create mode 100644 .cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-token-v2-trait.json create mode 100644 .cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-vrf-v2.clar create mode 100644 .cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-vrf-v2.json create mode 100644 .cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-auth-v2.clar create mode 100644 .cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-auth-v2.json create mode 100644 .cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-core-v2.clar create mode 100644 .cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-core-v2.json create mode 100644 .cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2.clar create mode 100644 .cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2.json create mode 100644 .cache/requirements/ST000000000000000000002AMW42H.pox.clar create mode 100644 .cache/requirements/ST000000000000000000002AMW42H.pox.json create mode 100644 .cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-auth-v2.clar create mode 100644 .cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-auth-v2.json create mode 100644 .cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-core-v2.clar create mode 100644 .cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-core-v2.json create mode 100644 .cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-token-v2.clar create mode 100644 .cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-token-v2.json create mode 100644 .cache/requirements/ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.nft-trait.clar create mode 100644 .cache/requirements/ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.nft-trait.json create mode 100644 .cache/requirements/ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.sip-010-trait-ft-standard.clar create mode 100644 .cache/requirements/ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.sip-010-trait-ft-standard.json create mode 100644 .cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-core-v2-trait.clar create mode 100644 .cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-core-v2-trait.json create mode 100644 .cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-token-v2-trait.clar create mode 100644 .cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-token-v2-trait.json create mode 100644 .cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-vrf-v2.clar create mode 100644 .cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-vrf-v2.json create mode 100644 .cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-auth-v2.clar create mode 100644 .cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-auth-v2.json create mode 100644 .cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-core-v2.clar create mode 100644 .cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-core-v2.json create mode 100644 .cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-token-v2.clar create mode 100644 .cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-token-v2.json diff --git a/.cache/requirements/SP000000000000000000002Q6VF78.pox.clar b/.cache/requirements/SP000000000000000000002Q6VF78.pox.clar new file mode 100644 index 0000000..21e0f69 --- /dev/null +++ b/.cache/requirements/SP000000000000000000002Q6VF78.pox.clar @@ -0,0 +1,695 @@ +;; PoX 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) + +;; The .pox contract +;; Error codes +(define-constant ERR_STACKING_UNREACHABLE 255) +(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) + +;; PoX disabling threshold (a percent) +(define-constant POX_REJECTION_FRACTION u25) + +;; 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) + +;; 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 + (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 configured true) + (ok true)) +) + +;; The Stacking lock-up state and associated metadata. +;; Records can be inserted into this map via one of two ways: +;; * via contract-call? to the (stack-stx) method, or +;; * via a transaction in the underlying burnchain that encodes the same data. +;; In the latter case, this map will be updated by the Stacks +;; node itself, and transactions in the burnchain will take priority +;; over transactions in the Stacks chain when processing this block. +(define-map stacking-state + { stacker: principal } + { + ;; how many uSTX locked? + amount-ustx: uint, + ;; 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, or p2wsh-p2sh UTXO, depending on the version. + pox-addr: { version: (buff 1), hashbytes: (buff 20) }, + ;; how long the uSTX are locked, in reward cycles. + lock-period: uint, + ;; reward cycle when rewards begin + first-reward-cycle: uint + } +) + +;; 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 20) }) + } +) + +;; 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 20) }, + total-ustx: uint + } +) + +(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 20) }, + 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-private (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-private (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-private (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-private (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))) + ;; is the caller allowance expired? + (if (< burn-block-height (unwrap! (get until-burn-ht caller-allowed) true)) + false + true)))) + +(define-private (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-private (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! +(define-private (append-reward-cycle-pox-addr (pox-addr (tuple (version (buff 1)) (hashbytes (buff 20)))) + (reward-cycle uint) + (amount-ustx uint)) + (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 }) + (map-set reward-cycle-pox-address-list-len + { reward-cycle: reward-cycle } + { len: (+ u1 sz) }) + (+ u1 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 })) + +;; Add a PoX address to the ith reward cycle, if i is between 0 and the given num-cycles (exclusive). +;; Arguments are given as a tuple, so this function can be (map ..)'ed onto a list of its arguments. +;; Used by add-pox-addr-to-reward-cycles. +;; No checking is done. +;; Returns 1 if added. +;; Returns 0 if not added. +(define-private (add-pox-addr-to-ith-reward-cycle (cycle-index uint) (params (tuple + (pox-addr (tuple (version (buff 1)) (hashbytes (buff 20)))) + (first-reward-cycle uint) + (num-cycles uint) + (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))) + { + pox-addr: (get pox-addr params), + first-reward-cycle: (get first-reward-cycle params), + num-cycles: num-cycles, + amount-ustx: (get amount-ustx params), + i: (if (< i num-cycles) + (let ((total-ustx (get-total-ustx-stacked reward-cycle))) + ;; 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)) + + ;; update running total + (map-set reward-cycle-total-stacked + { reward-cycle: reward-cycle } + { total-ustx: (+ (get amount-ustx params) total-ustx) }) + + ;; updated _this_ reward cycle + (+ i u1)) + (+ i u0)) + })) + +;; 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 20)))) + (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))) + ;; 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 (fold add-pox-addr-to-ith-reward-cycle cycle-indexes + { pox-addr: pox-addr, first-reward-cycle: first-reward-cycle, num-cycles: num-cycles, amount-ustx: amount-ustx, i: u0 }))) + (err ERR_STACKING_UNREACHABLE)) + (ok true))) + +(define-private (add-pox-partial-stacked-to-ith-cycle + (cycle-index uint) + (params { pox-addr: { version: (buff 1), hashbytes: (buff 20) }, + 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 20)))) + (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 burn address? +(define-private (check-pox-addr-version (version (buff 1))) + (or (is-eq version ADDRESS_VERSION_P2PKH) + (is-eq version ADDRESS_VERSION_P2SH) + (is-eq version ADDRESS_VERSION_P2WPKH) + (is-eq version ADDRESS_VERSION_P2WSH))) + +;; Is the given lock period valid? +(define-private (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 20)))) + (amount-ustx uint) + (first-reward-cycle uint) + (num-cycles uint)) + (begin + ;; minimum uSTX must be met + (asserts! (<= (print (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 20)))) + (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)) + (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 20)))) + (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 + (try! (add-pox-addr-to-reward-cycles pox-addr first-reward-cycle lock-period amount-ustx)) + + ;; add stacker record + (map-set stacking-state + { stacker: tx-sender } + { amount-ustx: amount-ustx, + pox-addr: pox-addr, + first-reward-cycle: first-reward-cycle, + lock-period: lock-period }) + + ;; 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 expiration +;; * 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 20) }))) + (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)) + + ;; 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. +;; 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 +(define-public (stack-aggregation-commit (pox-addr { version: (buff 1), hashbytes: (buff 20) }) + (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 + (add-pox-addr-to-ith-reward-cycle + u0 + { pox-addr: pox-addr, + first-reward-cycle: reward-cycle, + num-cycles: u1, + amount-ustx: amount-ustx, + i: 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 + (map-delete partial-stacked-by-cycle { pox-addr: pox-addr, sender: tx-sender, reward-cycle: reward-cycle }) + (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 20) }) + (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 } + { amount-ustx: amount-ustx, + pox-addr: pox-addr, + first-reward-cycle: first-reward-cycle, + lock-period: lock-period }) + + ;; 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, + }) +) diff --git a/.cache/requirements/SP000000000000000000002Q6VF78.pox.json b/.cache/requirements/SP000000000000000000002Q6VF78.pox.json new file mode 100644 index 0000000..11162a1 --- /dev/null +++ b/.cache/requirements/SP000000000000000000002Q6VF78.pox.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch20", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-auth-v2.clar b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-auth-v2.clar new file mode 100644 index 0000000..296bdb5 --- /dev/null +++ b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-auth-v2.clar @@ -0,0 +1,630 @@ +;; MIAMICOIN AUTH CONTRACT V2 +;; CityCoins Protocol Version 2.0.0 + +(define-constant CONTRACT_OWNER tx-sender) + +;; TRAIT DEFINITIONS + +(use-trait coreTraitV2 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-core-v2-trait.citycoin-core-v2) +(use-trait tokenTraitV2 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-token-v2-trait.citycoin-token-v2) + +;; ERRORS + +(define-constant ERR_UNKNOWN_JOB (err u6000)) +(define-constant ERR_UNAUTHORIZED (err u6001)) +(define-constant ERR_JOB_IS_ACTIVE (err u6002)) +(define-constant ERR_JOB_IS_NOT_ACTIVE (err u6003)) +(define-constant ERR_ALREADY_VOTED_THIS_WAY (err u6004)) +(define-constant ERR_JOB_IS_EXECUTED (err u6005)) +(define-constant ERR_JOB_IS_NOT_APPROVED (err u6006)) +(define-constant ERR_ARGUMENT_ALREADY_EXISTS (err u6007)) +(define-constant ERR_NO_ACTIVE_CORE_CONTRACT (err u6008)) +(define-constant ERR_CORE_CONTRACT_NOT_FOUND (err u6009)) +(define-constant ERR_UNKNOWN_ARGUMENT (err u6010)) +(define-constant ERR_INCORRECT_CONTRACT_STATE (err u6011)) +(define-constant ERR_CONTRACT_ALREADY_EXISTS (err u6012)) + +;; JOB MANAGEMENT + +(define-constant REQUIRED_APPROVALS u3) + +(define-data-var lastJobId uint u0) + +(define-map Jobs + uint + { + creator: principal, + name: (string-ascii 255), + target: principal, + approvals: uint, + disapprovals: uint, + isActive: bool, + isExecuted: bool + } +) + +(define-map JobApprovers + { jobId: uint, approver: principal } + bool +) + +(define-map Approvers + principal + bool +) + +(define-map ArgumentLastIdsByType + { jobId: uint, argumentType: (string-ascii 25) } + uint +) + +(define-map UIntArgumentsByName + { jobId: uint, argumentName: (string-ascii 255) } + { argumentId: uint, value: uint} +) + +(define-map UIntArgumentsById + { jobId: uint, argumentId: uint } + { argumentName: (string-ascii 255), value: uint } +) + +(define-map PrincipalArgumentsByName + { jobId: uint, argumentName: (string-ascii 255) } + { argumentId: uint, value: principal } +) + +(define-map PrincipalArgumentsById + { jobId: uint, argumentId: uint } + { argumentName: (string-ascii 255), value: principal } +) + +;; FUNCTIONS + +(define-read-only (get-last-job-id) + (var-get lastJobId) +) + +(define-public (create-job (name (string-ascii 255)) (target principal)) + (let + ( + (newJobId (+ (var-get lastJobId) u1)) + ) + (asserts! (is-approver tx-sender) ERR_UNAUTHORIZED) + (map-set Jobs + newJobId + { + creator: tx-sender, + name: name, + target: target, + approvals: u0, + disapprovals: u0, + isActive: false, + isExecuted: false + } + ) + (var-set lastJobId newJobId) + (ok newJobId) + ) +) + +(define-read-only (get-job (jobId uint)) + (map-get? Jobs jobId) +) + +(define-public (activate-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + ) + (asserts! (is-eq (get creator job) tx-sender) ERR_UNAUTHORIZED) + (asserts! (not (get isActive job)) ERR_JOB_IS_ACTIVE) + (map-set Jobs + jobId + (merge job { isActive: true }) + ) + (ok true) + ) +) + +(define-public (approve-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + (previousVote (map-get? JobApprovers { jobId: jobId, approver: tx-sender })) + ) + (asserts! (get isActive job) ERR_JOB_IS_NOT_ACTIVE) + (asserts! (is-approver tx-sender) ERR_UNAUTHORIZED) + ;; save vote + (map-set JobApprovers + { jobId: jobId, approver: tx-sender } + true + ) + (match previousVote approved + (begin + (asserts! (not approved) ERR_ALREADY_VOTED_THIS_WAY) + (map-set Jobs jobId + (merge job + { + approvals: (+ (get approvals job) u1), + disapprovals: (- (get disapprovals job) u1) + } + ) + ) + ) + ;; no previous vote + (map-set Jobs + jobId + (merge job { approvals: (+ (get approvals job) u1) } ) + ) + ) + (ok true) + ) +) + +(define-public (disapprove-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + (previousVote (map-get? JobApprovers { jobId: jobId, approver: tx-sender })) + ) + (asserts! (get isActive job) ERR_JOB_IS_NOT_ACTIVE) + (asserts! (is-approver tx-sender) ERR_UNAUTHORIZED) + ;; save vote + (map-set JobApprovers + { jobId: jobId, approver: tx-sender } + false + ) + (match previousVote approved + (begin + (asserts! approved ERR_ALREADY_VOTED_THIS_WAY) + (map-set Jobs jobId + (merge job + { + approvals: (- (get approvals job) u1), + disapprovals: (+ (get disapprovals job) u1) + } + ) + ) + ) + ;; no previous vote + (map-set Jobs + jobId + (merge job { disapprovals: (+ (get disapprovals job) u1) } ) + ) + ) + (ok true) + ) +) + +(define-read-only (is-job-approved (jobId uint)) + (match (get-job jobId) job + (>= (get approvals job) REQUIRED_APPROVALS) + false + ) +) + +(define-public (mark-job-as-executed (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + ) + (asserts! (get isActive job) ERR_JOB_IS_NOT_ACTIVE) + (asserts! (>= (get approvals job) REQUIRED_APPROVALS) ERR_JOB_IS_NOT_APPROVED) + (asserts! (is-eq (get target job) contract-caller) ERR_UNAUTHORIZED) + (asserts! (not (get isExecuted job)) ERR_JOB_IS_EXECUTED) + (map-set Jobs + jobId + (merge job { isExecuted: true }) + ) + (ok true) + ) +) + +(define-public (add-uint-argument (jobId uint) (argumentName (string-ascii 255)) (value uint)) + (let + ( + (argumentId (generate-argument-id jobId "uint")) + ) + (try! (guard-add-argument jobId)) + (asserts! + (and + (map-insert UIntArgumentsById + { jobId: jobId, argumentId: argumentId } + { argumentName: argumentName, value: value } + ) + (map-insert UIntArgumentsByName + { jobId: jobId, argumentName: argumentName } + { argumentId: argumentId, value: value} + ) + ) + ERR_ARGUMENT_ALREADY_EXISTS + ) + (ok true) + ) +) + +(define-read-only (get-uint-argument-by-name (jobId uint) (argumentName (string-ascii 255))) + (map-get? UIntArgumentsByName { jobId: jobId, argumentName: argumentName }) +) + +(define-read-only (get-uint-argument-by-id (jobId uint) (argumentId uint)) + (map-get? UIntArgumentsById { jobId: jobId, argumentId: argumentId }) +) + +(define-read-only (get-uint-value-by-name (jobId uint) (argumentName (string-ascii 255))) + (get value (get-uint-argument-by-name jobId argumentName)) +) + +(define-read-only (get-uint-value-by-id (jobId uint) (argumentId uint)) + (get value (get-uint-argument-by-id jobId argumentId)) +) + +(define-public (add-principal-argument (jobId uint) (argumentName (string-ascii 255)) (value principal)) + (let + ( + (argumentId (generate-argument-id jobId "principal")) + ) + (try! (guard-add-argument jobId)) + (asserts! + (and + (map-insert PrincipalArgumentsById + { jobId: jobId, argumentId: argumentId } + { argumentName: argumentName, value: value } + ) + (map-insert PrincipalArgumentsByName + { jobId: jobId, argumentName: argumentName } + { argumentId: argumentId, value: value} + ) + ) + ERR_ARGUMENT_ALREADY_EXISTS + ) + (ok true) + ) +) + +(define-read-only (get-principal-argument-by-name (jobId uint) (argumentName (string-ascii 255))) + (map-get? PrincipalArgumentsByName { jobId: jobId, argumentName: argumentName }) +) + +(define-read-only (get-principal-argument-by-id (jobId uint) (argumentId uint)) + (map-get? PrincipalArgumentsById { jobId: jobId, argumentId: argumentId }) +) + +(define-read-only (get-principal-value-by-name (jobId uint) (argumentName (string-ascii 255))) + (get value (get-principal-argument-by-name jobId argumentName)) +) + +(define-read-only (get-principal-value-by-id (jobId uint) (argumentId uint)) + (get value (get-principal-argument-by-id jobId argumentId)) +) + +;; PRIVATE FUNCTIONS + +(define-read-only (is-approver (user principal)) + (default-to false (map-get? Approvers user)) +) + +(define-private (generate-argument-id (jobId uint) (argumentType (string-ascii 25))) + (let + ( + (argumentId (+ (default-to u0 (map-get? ArgumentLastIdsByType { jobId: jobId, argumentType: argumentType })) u1)) + ) + (map-set ArgumentLastIdsByType + { jobId: jobId, argumentType: argumentType } + argumentId + ) + ;; return + argumentId + ) +) + +(define-private (guard-add-argument (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + ) + (asserts! (not (get isActive job)) ERR_JOB_IS_ACTIVE) + (asserts! (is-eq (get creator job) contract-caller) ERR_UNAUTHORIZED) + (ok true) + ) +) + +;; CONTRACT MANAGEMENT + +;; initial value for active core contract +;; set to deployer address at startup to prevent +;; circular dependency of core on auth +(define-data-var activeCoreContract principal CONTRACT_OWNER) +(define-data-var initialized bool false) + +;; core contract states +(define-constant STATE_DEPLOYED u0) +(define-constant STATE_ACTIVE u1) +(define-constant STATE_INACTIVE u2) + +;; core contract map +(define-map CoreContracts + principal + { + state: uint, + startHeight: uint, + endHeight: uint + } +) + +;; getter for active core contract +(define-read-only (get-active-core-contract) + (begin + (asserts! (not (is-eq (var-get activeCoreContract) CONTRACT_OWNER)) ERR_NO_ACTIVE_CORE_CONTRACT) + (ok (var-get activeCoreContract)) + ) +) + +;; getter for core contract map +(define-read-only (get-core-contract-info (targetContract principal)) + (let + ( + (coreContract (unwrap! (map-get? CoreContracts targetContract) ERR_CORE_CONTRACT_NOT_FOUND)) + ) + (ok coreContract) + ) +) + +;; one-time function to initialize contracts after all contracts are deployed +;; - check that deployer is calling this function +;; - check this contract is not activated already (one-time use) +;; - set initial map value for core contract v1 +;; - set cityWallet in core contract +;; - set intialized true +(define-public (initialize-contracts (coreContract )) + (let + ( + (coreContractAddress (contract-of coreContract)) + ) + (asserts! (is-eq contract-caller CONTRACT_OWNER) ERR_UNAUTHORIZED) + (asserts! (not (var-get initialized)) ERR_UNAUTHORIZED) + (map-set CoreContracts + coreContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (try! (contract-call? coreContract set-city-wallet (var-get cityWallet))) + (var-set initialized true) + (ok true) + ) +) + +(define-read-only (is-initialized) + (var-get initialized) +) + +;; function to activate core contract through registration +;; - check that target is in core contract map +;; - check that caller is core contract +;; - check that target is in STATE_DEPLOYED +;; - set active in core contract map +;; - set as activeCoreContract +(define-public (activate-core-contract (targetContract principal) (stacksHeight uint)) + (let + ( + (coreContract (unwrap! (map-get? CoreContracts targetContract) ERR_CORE_CONTRACT_NOT_FOUND)) + ) + (asserts! (is-eq (get state coreContract) STATE_DEPLOYED) ERR_INCORRECT_CONTRACT_STATE) + (asserts! (is-eq contract-caller targetContract) ERR_UNAUTHORIZED) + (map-set CoreContracts + targetContract + { + state: STATE_ACTIVE, + startHeight: stacksHeight, + endHeight: u0 + }) + (var-set activeCoreContract targetContract) + (ok true) + ) +) + +;; protected function to update core contract +(define-public (upgrade-core-contract (oldContract ) (newContract )) + (let + ( + (oldContractAddress (contract-of oldContract)) + (oldContractMap (unwrap! (map-get? CoreContracts oldContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + (newContractAddress (contract-of newContract)) + ) + (asserts! (not (is-eq oldContractAddress newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (is-none (map-get? CoreContracts newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + (map-set CoreContracts + oldContractAddress + { + state: STATE_INACTIVE, + startHeight: (get startHeight oldContractMap), + endHeight: block-height + }) + (map-set CoreContracts + newContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (var-set activeCoreContract newContractAddress) + (try! (contract-call? oldContract shutdown-contract block-height)) + (try! (contract-call? newContract set-city-wallet (var-get cityWallet))) + (ok true) + ) +) + +(define-public (execute-upgrade-core-contract-job (jobId uint) (oldContract ) (newContract )) + (let + ( + (oldContractArg (unwrap! (get-principal-value-by-name jobId "oldContract") ERR_UNKNOWN_ARGUMENT)) + (newContractArg (unwrap! (get-principal-value-by-name jobId "newContract") ERR_UNKNOWN_ARGUMENT)) + (oldContractAddress (contract-of oldContract)) + (oldContractMap (unwrap! (map-get? CoreContracts oldContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + (newContractAddress (contract-of newContract)) + ) + (asserts! (not (is-eq oldContractAddress newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (is-none (map-get? CoreContracts newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (and (is-eq oldContractArg oldContractAddress) (is-eq newContractArg newContractAddress)) ERR_UNAUTHORIZED) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (map-set CoreContracts + oldContractAddress + { + state: STATE_INACTIVE, + startHeight: (get startHeight oldContractMap), + endHeight: block-height + }) + (map-set CoreContracts + newContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (var-set activeCoreContract newContractAddress) + (try! (contract-call? oldContract shutdown-contract block-height)) + (try! (contract-call? newContract set-city-wallet (var-get cityWallet))) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; CITY WALLET MANAGEMENT + +;; initial value for city wallet +(define-data-var cityWallet principal 'SM2MARAVW6BEJCD13YV2RHGYHQWT7TDDNMNRB1MVT) + +;; returns city wallet principal +(define-read-only (get-city-wallet) + (ok (var-get cityWallet)) +) + +;; protected function to update city wallet variable +(define-public (set-city-wallet (targetContract ) (newCityWallet principal)) + (let + ( + (coreContractAddress (contract-of targetContract)) + (coreContract (unwrap! (map-get? CoreContracts coreContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + ) + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + (asserts! (is-eq coreContractAddress (var-get activeCoreContract)) ERR_UNAUTHORIZED) + (var-set cityWallet newCityWallet) + (try! (contract-call? targetContract set-city-wallet newCityWallet)) + (ok true) + ) +) + +(define-public (execute-set-city-wallet-job (jobId uint) (targetContract )) + (let + ( + (coreContractAddress (contract-of targetContract)) + (coreContract (unwrap! (map-get? CoreContracts coreContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + (newCityWallet (unwrap! (get-principal-value-by-name jobId "newCityWallet") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (asserts! (is-eq coreContractAddress (var-get activeCoreContract)) ERR_UNAUTHORIZED) + (var-set cityWallet newCityWallet) + (try! (contract-call? targetContract set-city-wallet newCityWallet)) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; check if contract caller is city wallet +(define-private (is-authorized-city) + (is-eq contract-caller (var-get cityWallet)) +) + +;; TOKEN MANAGEMENT + +(define-public (set-token-uri (targetContract ) (newUri (optional (string-utf8 256)))) + (begin + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + (as-contract (try! (contract-call? targetContract set-token-uri newUri))) + (ok true) + ) +) + +;; COINBASE THRESHOLDS + +(define-public (update-coinbase-thresholds (targetCore ) (targetToken ) (threshold1 uint) (threshold2 uint) (threshold3 uint) (threshold4 uint) (threshold5 uint)) + (begin + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + ;; update in token contract + (as-contract (try! (contract-call? targetToken update-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5))) + ;; update core contract based on token contract + (as-contract (try! (contract-call? targetCore update-coinbase-thresholds))) + (ok true) + ) +) + +(define-public (execute-update-coinbase-thresholds-job (jobId uint) (targetCore ) (targetToken )) + (let + ( + (threshold1 (unwrap! (get-uint-value-by-name jobId "threshold1") ERR_UNKNOWN_ARGUMENT)) + (threshold2 (unwrap! (get-uint-value-by-name jobId "threshold2") ERR_UNKNOWN_ARGUMENT)) + (threshold3 (unwrap! (get-uint-value-by-name jobId "threshold3") ERR_UNKNOWN_ARGUMENT)) + (threshold4 (unwrap! (get-uint-value-by-name jobId "threshold4") ERR_UNKNOWN_ARGUMENT)) + (threshold5 (unwrap! (get-uint-value-by-name jobId "threshold5") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (as-contract (try! (contract-call? targetToken update-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5))) + (as-contract (try! (contract-call? targetCore update-coinbase-thresholds))) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; COINBASE AMOUNTS (REWARDS) + +(define-public (update-coinbase-amounts (targetCore ) (targetToken ) (amountBonus uint) (amount1 uint) (amount2 uint) (amount3 uint) (amount4 uint) (amount5 uint) (amountDefault uint)) + (begin + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + ;; update in token contract + (as-contract (try! (contract-call? targetToken update-coinbase-amounts amountBonus amount1 amount2 amount3 amount4 amount5 amountDefault))) + ;; update core contract based on token contract + (as-contract (try! (contract-call? targetCore update-coinbase-amounts))) + (ok true) + ) +) + +(define-public (execute-update-coinbase-amounts-job (jobId uint) (targetCore ) (targetToken )) + (let + ( + (amountBonus (unwrap! (get-uint-value-by-name jobId "amountBonus") ERR_UNKNOWN_ARGUMENT)) + (amount1 (unwrap! (get-uint-value-by-name jobId "amount1") ERR_UNKNOWN_ARGUMENT)) + (amount2 (unwrap! (get-uint-value-by-name jobId "amount2") ERR_UNKNOWN_ARGUMENT)) + (amount3 (unwrap! (get-uint-value-by-name jobId "amount3") ERR_UNKNOWN_ARGUMENT)) + (amount4 (unwrap! (get-uint-value-by-name jobId "amount4") ERR_UNKNOWN_ARGUMENT)) + (amount5 (unwrap! (get-uint-value-by-name jobId "amount5") ERR_UNKNOWN_ARGUMENT)) + (amountDefault (unwrap! (get-uint-value-by-name jobId "amountDefault") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (as-contract (try! (contract-call? targetToken update-coinbase-amounts amountBonus amount1 amount2 amount3 amount4 amount5 amountDefault))) + (as-contract (try! (contract-call? targetCore update-coinbase-amounts))) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; APPROVERS MANAGEMENT + +(define-public (execute-replace-approver-job (jobId uint)) + (let + ( + (oldApprover (unwrap! (get-principal-value-by-name jobId "oldApprover") ERR_UNKNOWN_ARGUMENT)) + (newApprover (unwrap! (get-principal-value-by-name jobId "newApprover") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (map-set Approvers oldApprover false) + (map-set Approvers newApprover true) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; CONTRACT INITIALIZATION + +(map-insert Approvers 'SP372JVX6EWE2M0XPA84MWZYRRG2M6CAC4VVC12V1 true) +(map-insert Approvers 'SP2R0DQYR7XHD161SH2GK49QRP1YSV7HE9JSG7W6G true) +(map-insert Approvers 'SPN4Y5QPGQA8882ZXW90ADC2DHYXMSTN8VAR8C3X true) +(map-insert Approvers 'SP3YYGCGX1B62CYAH4QX7PQE63YXG7RDTXD8BQHJQ true) +(map-insert Approvers 'SP7DGES13508FHRWS1FB0J3SZA326FP6QRMB6JDE true) diff --git a/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-auth-v2.json b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-auth-v2.json new file mode 100644 index 0000000..89c8a70 --- /dev/null +++ b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-auth-v2.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v1-patch.clar b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v1-patch.clar new file mode 100644 index 0000000..f97ec9a --- /dev/null +++ b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v1-patch.clar @@ -0,0 +1,67 @@ +;; MIAMICOIN CORE CONTRACT V1 PATCH +;; CityCoins Protocol Version 2.0.0 + +(impl-trait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.citycoin-core) + +;; uses same and skips errors already defined in miamicoin-core-v1 +(define-constant ERR_UNAUTHORIZED (err u1001)) +;; generic error used to disable all functions below +(define-constant ERR_CONTRACT_DISABLED (err u1021)) + +;; DISABLED FUNCTIONS + +(define-public (register-user (memo (optional (string-utf8 50)))) + ERR_CONTRACT_DISABLED +) + +(define-public (mine-tokens (amountUstx uint) (memo (optional (buff 34)))) + ERR_CONTRACT_DISABLED +) + +(define-public (claim-mining-reward (minerBlockHeight uint)) + ERR_CONTRACT_DISABLED +) + +(define-public (stack-tokens (amountTokens uint) (lockPeriod uint)) + ERR_CONTRACT_DISABLED +) + +(define-public (claim-stacking-reward (targetCycle uint)) + ERR_CONTRACT_DISABLED +) + +(define-public (shutdown-contract (stacksHeight uint)) + ERR_CONTRACT_DISABLED +) + +;; need to allow function to succeed one time in order to be updated +;; as the new V1 core contract, then will fail after that +(define-data-var upgraded bool false) + +(define-public (set-city-wallet (newCityWallet principal)) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (if (var-get upgraded) + ;; if true + ERR_CONTRACT_DISABLED + ;; if false + (ok (var-set upgraded true)) + ) + ) +) + +;; checks if caller is auth contract +(define-private (is-authorized-auth) + (is-eq contract-caller 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-auth) +) + +;; V1 TO V2 CONVERSION + +;; pass-through function to allow burning MIA v1 +(define-public (burn-mia-v1 (amount uint) (owner principal)) + (begin + (asserts! (is-eq tx-sender owner) ERR_UNAUTHORIZED) + (as-contract (try! (contract-call? 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-token burn amount owner))) + (ok true) + ) +) diff --git a/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v1-patch.json b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v1-patch.json new file mode 100644 index 0000000..89c8a70 --- /dev/null +++ b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v1-patch.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v2.clar b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v2.clar new file mode 100644 index 0000000..f73fa55 --- /dev/null +++ b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v2.clar @@ -0,0 +1,1008 @@ +;; MIAMICOIN CORE CONTRACT V2 +;; CityCoins Protocol Version 2.0.0 + +;; GENERAL CONFIGURATION + +(impl-trait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.citycoin-core) +(impl-trait 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-core-v2-trait.citycoin-core-v2) +(define-constant CONTRACT_OWNER tx-sender) + +;; ERROR CODES + +(define-constant ERR_UNAUTHORIZED (err u1000)) +(define-constant ERR_USER_ALREADY_REGISTERED (err u1001)) +(define-constant ERR_USER_NOT_FOUND (err u1002)) +(define-constant ERR_USER_ID_NOT_FOUND (err u1003)) +(define-constant ERR_ACTIVATION_THRESHOLD_REACHED (err u1004)) +(define-constant ERR_CONTRACT_NOT_ACTIVATED (err u1005)) +(define-constant ERR_USER_ALREADY_MINED (err u1006)) +(define-constant ERR_INSUFFICIENT_COMMITMENT (err u1007)) +(define-constant ERR_INSUFFICIENT_BALANCE (err u1008)) +(define-constant ERR_USER_DID_NOT_MINE_IN_BLOCK (err u1009)) +(define-constant ERR_CLAIMED_BEFORE_MATURITY (err u1010)) +(define-constant ERR_NO_MINERS_AT_BLOCK (err u1011)) +(define-constant ERR_REWARD_ALREADY_CLAIMED (err u1012)) +(define-constant ERR_MINER_DID_NOT_WIN (err u1013)) +(define-constant ERR_NO_VRF_SEED_FOUND (err u1014)) +(define-constant ERR_STACKING_NOT_AVAILABLE (err u1015)) +(define-constant ERR_CANNOT_STACK (err u1016)) +(define-constant ERR_REWARD_CYCLE_NOT_COMPLETED (err u1017)) +(define-constant ERR_NOTHING_TO_REDEEM (err u1018)) +(define-constant ERR_UNABLE_TO_FIND_CITY_WALLET (err u1019)) +(define-constant ERR_CLAIM_IN_WRONG_CONTRACT (err u1020)) +(define-constant ERR_BLOCK_HEIGHT_IN_PAST (err u1021)) +(define-constant ERR_COINBASE_AMOUNTS_NOT_FOUND (err u1022)) + +;; CITY WALLET MANAGEMENT + +;; initial value for city wallet, set to this contract until initialized +(define-data-var cityWallet principal 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v2) + +;; returns set city wallet principal +(define-read-only (get-city-wallet) + (var-get cityWallet) +) + +;; protected function to update city wallet variable +(define-public (set-city-wallet (newCityWallet principal)) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (ok (var-set cityWallet newCityWallet)) + ) +) + +;; REGISTRATION + +(define-constant MIAMICOIN_ACTIVATION_HEIGHT u24497) +(define-data-var activationBlock uint u340282366920938463463374607431768211455) +(define-data-var activationDelay uint u0) +(define-data-var activationReached bool false) +(define-data-var activationTarget uint u0) +(define-data-var activationThreshold uint u20) +(define-data-var usersNonce uint u0) + +;; returns Stacks block height registration was activated at plus activationDelay +(define-read-only (get-activation-block) + (begin + (asserts! (get-activation-status) ERR_CONTRACT_NOT_ACTIVATED) + (ok (var-get activationBlock)) + ) +) + +;; returns activation delay +(define-read-only (get-activation-delay) + (var-get activationDelay) +) + +;; returns activation status as boolean +(define-read-only (get-activation-status) + (var-get activationReached) +) + +;; returns activation target +(define-read-only (get-activation-target) + (begin + (asserts! (get-activation-status) ERR_CONTRACT_NOT_ACTIVATED) + (ok (var-get activationTarget)) + ) +) + +;; returns activation threshold +(define-read-only (get-activation-threshold) + (var-get activationThreshold) +) + +;; returns number of registered users, used for activation and tracking user IDs +(define-read-only (get-registered-users-nonce) + (var-get usersNonce) +) + +;; store user principal by user id +(define-map Users + uint + principal +) + +;; store user id by user principal +(define-map UserIds + principal + uint +) + +;; returns (some userId) or none +(define-read-only (get-user-id (user principal)) + (map-get? UserIds user) +) + +;; returns (some userPrincipal) or none +(define-read-only (get-user (userId uint)) + (map-get? Users userId) +) + +;; returns user ID if it has been created, or creates and returns new ID +(define-private (get-or-create-user-id (user principal)) + (match + (map-get? UserIds user) + value value + (let + ( + (newId (+ u1 (var-get usersNonce))) + ) + (map-set Users newId user) + (map-set UserIds user newId) + (var-set usersNonce newId) + newId + ) + ) +) + +;; registers users that signal activation of contract until threshold is met +(define-public (register-user (memo (optional (string-utf8 50)))) + (let + ( + (newId (+ u1 (var-get usersNonce))) + (threshold (var-get activationThreshold)) + (initialized (contract-call? 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-auth-v2 is-initialized)) + ) + + (asserts! initialized ERR_UNAUTHORIZED) + + (asserts! (is-none (map-get? UserIds tx-sender)) + ERR_USER_ALREADY_REGISTERED) + + (asserts! (<= newId threshold) + ERR_ACTIVATION_THRESHOLD_REACHED) + + (if (is-some memo) + (print memo) + none + ) + + (get-or-create-user-id tx-sender) + + (if (is-eq newId threshold) + (let + ( + (activationTargetBlock (+ block-height (var-get activationDelay))) + ) + (try! (contract-call? 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-auth-v2 activate-core-contract (as-contract tx-sender) activationTargetBlock)) + (try! (contract-call? 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2 activate-token (as-contract tx-sender) MIAMICOIN_ACTIVATION_HEIGHT)) + (try! (set-coinbase-thresholds)) + (try! (set-coinbase-amounts)) + (var-set activationReached true) + (var-set activationBlock MIAMICOIN_ACTIVATION_HEIGHT) + (var-set activationTarget activationTargetBlock) + (ok true) + ) + (ok true) + ) + ) +) + +;; MINING CONFIGURATION + +;; define split to custodied wallet address for the city +(define-constant SPLIT_CITY_PCT u30) + +;; how long a miner must wait before block winner can claim their minted tokens +(define-data-var tokenRewardMaturity uint u100) + +;; At a given Stacks block height: +;; - how many miners were there +;; - what was the total amount submitted +;; - what was the total amount submitted to the city +;; - what was the total amount submitted to Stackers +;; - was the block reward claimed +(define-map MiningStatsAtBlock + uint + { + minersCount: uint, + amount: uint, + amountToCity: uint, + amountToStackers: uint, + rewardClaimed: bool + } +) + +;; returns map MiningStatsAtBlock at a given Stacks block height if it exists +(define-read-only (get-mining-stats-at-block (stacksHeight uint)) + (map-get? MiningStatsAtBlock stacksHeight) +) + +;; returns map MiningStatsAtBlock at a given Stacks block height +;; or, an empty structure +(define-read-only (get-mining-stats-at-block-or-default (stacksHeight uint)) + (default-to { + minersCount: u0, + amount: u0, + amountToCity: u0, + amountToStackers: u0, + rewardClaimed: false + } + (map-get? MiningStatsAtBlock stacksHeight) + ) +) + +;; At a given Stacks block height and user ID: +;; - what is their ustx commitment +;; - what are the low/high values (used for VRF) +(define-map MinersAtBlock + { + stacksHeight: uint, + userId: uint + } + { + ustx: uint, + lowValue: uint, + highValue: uint, + winner: bool + } +) + +;; returns true if a given miner has already mined at a given block height +(define-read-only (has-mined-at-block (stacksHeight uint) (userId uint)) + (is-some + (map-get? MinersAtBlock { stacksHeight: stacksHeight, userId: userId }) + ) +) + +;; returns map MinersAtBlock at a given Stacks block height for a user ID +(define-read-only (get-miner-at-block (stacksHeight uint) (userId uint)) + (map-get? MinersAtBlock { stacksHeight: stacksHeight, userId: userId }) +) + +;; returns map MinersAtBlock at a given Stacks block height for a user ID +;; or, an empty structure +(define-read-only (get-miner-at-block-or-default (stacksHeight uint) (userId uint)) + (default-to { + highValue: u0, + lowValue: u0, + ustx: u0, + winner: false + } + (map-get? MinersAtBlock { stacksHeight: stacksHeight, userId: userId })) +) + +;; At a given Stacks block height: +;; - what is the max highValue from MinersAtBlock (used for VRF) +(define-map MinersAtBlockHighValue + uint + uint +) + +;; returns last high value from map MinersAtBlockHighValue +(define-read-only (get-last-high-value-at-block (stacksHeight uint)) + (default-to u0 + (map-get? MinersAtBlockHighValue stacksHeight)) +) + +;; At a given Stacks block height: +;; - what is the userId of miner who won this block +(define-map BlockWinnerIds + uint + uint +) + +(define-read-only (get-block-winner-id (stacksHeight uint)) + (map-get? BlockWinnerIds stacksHeight) +) + +;; MINING ACTIONS + +(define-public (mine-tokens (amountUstx uint) (memo (optional (buff 34)))) + (let + ( + (userId (get-or-create-user-id tx-sender)) + ) + (try! (mine-tokens-at-block userId block-height amountUstx memo)) + (ok true) + ) +) + +(define-public (mine-many (amounts (list 200 uint))) + (begin + (asserts! (is-activated) ERR_CONTRACT_NOT_ACTIVATED) + (asserts! (> (len amounts) u0) ERR_INSUFFICIENT_COMMITMENT) + (match (fold mine-single amounts (ok { userId: (get-or-create-user-id tx-sender), toStackers: u0, toCity: u0, stacksHeight: block-height })) + okReturn + (begin + (asserts! (>= (stx-get-balance tx-sender) (+ (get toStackers okReturn) (get toCity okReturn))) ERR_INSUFFICIENT_BALANCE) + (if (> (get toStackers okReturn ) u0) + (try! (stx-transfer? (get toStackers okReturn ) tx-sender (as-contract tx-sender))) + false + ) + (try! (stx-transfer? (get toCity okReturn) tx-sender (var-get cityWallet))) + (print { + firstBlock: block-height, + lastBlock: (- (+ block-height (len amounts)) u1) + }) + (ok true) + ) + errReturn (err errReturn) + ) + ) +) + +(define-private (mine-single + (amountUstx uint) + (return (response + { + userId: uint, + toStackers: uint, + toCity: uint, + stacksHeight: uint + } + uint + ))) + + (match return okReturn + (let + ( + (stacksHeight (get stacksHeight okReturn)) + (rewardCycle (default-to u0 (get-reward-cycle stacksHeight))) + (stackingActive (stacking-active-at-cycle rewardCycle)) + (toCity + (if stackingActive + (/ (* SPLIT_CITY_PCT amountUstx) u100) + amountUstx + ) + ) + (toStackers (- amountUstx toCity)) + ) + (asserts! (not (has-mined-at-block stacksHeight (get userId okReturn))) ERR_USER_ALREADY_MINED) + (asserts! (> amountUstx u0) ERR_INSUFFICIENT_COMMITMENT) + (try! (set-tokens-mined (get userId okReturn) stacksHeight amountUstx toStackers toCity)) + (ok (merge okReturn + { + toStackers: (+ (get toStackers okReturn) toStackers), + toCity: (+ (get toCity okReturn) toCity), + stacksHeight: (+ stacksHeight u1) + } + )) + ) + errReturn (err errReturn) + ) +) + +(define-private (mine-tokens-at-block (userId uint) (stacksHeight uint) (amountUstx uint) (memo (optional (buff 34)))) + (let + ( + (rewardCycle (default-to u0 (get-reward-cycle stacksHeight))) + (stackingActive (stacking-active-at-cycle rewardCycle)) + (toCity + (if stackingActive + (/ (* SPLIT_CITY_PCT amountUstx) u100) + amountUstx + ) + ) + (toStackers (- amountUstx toCity)) + ) + (asserts! (is-activated) ERR_CONTRACT_NOT_ACTIVATED) + (asserts! (not (has-mined-at-block stacksHeight userId)) ERR_USER_ALREADY_MINED) + (asserts! (> amountUstx u0) ERR_INSUFFICIENT_COMMITMENT) + (asserts! (>= (stx-get-balance tx-sender) amountUstx) ERR_INSUFFICIENT_BALANCE) + (try! (set-tokens-mined userId stacksHeight amountUstx toStackers toCity)) + (if (is-some memo) + (print memo) + none + ) + (if stackingActive + (try! (stx-transfer? toStackers tx-sender (as-contract tx-sender))) + false + ) + (try! (stx-transfer? toCity tx-sender (var-get cityWallet))) + (ok true) + ) +) + +(define-private (set-tokens-mined (userId uint) (stacksHeight uint) (amountUstx uint) (toStackers uint) (toCity uint)) + (let + ( + (blockStats (get-mining-stats-at-block-or-default stacksHeight)) + (newMinersCount (+ (get minersCount blockStats) u1)) + (minerLowVal (get-last-high-value-at-block stacksHeight)) + (rewardCycle (unwrap! (get-reward-cycle stacksHeight) + ERR_STACKING_NOT_AVAILABLE)) + (rewardCycleStats (get-stacking-stats-at-cycle-or-default rewardCycle)) + ) + (map-set MiningStatsAtBlock + stacksHeight + { + minersCount: newMinersCount, + amount: (+ (get amount blockStats) amountUstx), + amountToCity: (+ (get amountToCity blockStats) toCity), + amountToStackers: (+ (get amountToStackers blockStats) toStackers), + rewardClaimed: false + } + ) + (map-set MinersAtBlock + { + stacksHeight: stacksHeight, + userId: userId + } + { + ustx: amountUstx, + lowValue: (if (> minerLowVal u0) (+ minerLowVal u1) u0), + highValue: (+ minerLowVal amountUstx), + winner: false + } + ) + (map-set MinersAtBlockHighValue + stacksHeight + (+ minerLowVal amountUstx) + ) + (if (> toStackers u0) + (map-set StackingStatsAtCycle + rewardCycle + { + amountUstx: (+ (get amountUstx rewardCycleStats) toStackers), + amountToken: (get amountToken rewardCycleStats) + } + ) + false + ) + (ok true) + ) +) + +;; MINING REWARD CLAIM ACTIONS + +;; calls function to claim mining reward in active logic contract +(define-public (claim-mining-reward (minerBlockHeight uint)) + (begin + (asserts! (or (is-eq (var-get shutdownHeight) u0) (< minerBlockHeight (var-get shutdownHeight))) ERR_CLAIM_IN_WRONG_CONTRACT) + (try! (claim-mining-reward-at-block tx-sender block-height minerBlockHeight)) + (ok true) + ) +) + +;; Determine whether or not the given principal can claim the mined tokens at a particular block height, +;; given the miners record for that block height, a random sample, and the current block height. +(define-private (claim-mining-reward-at-block (user principal) (stacksHeight uint) (minerBlockHeight uint)) + (let + ( + (maturityHeight (+ (var-get tokenRewardMaturity) minerBlockHeight)) + (userId (unwrap! (get-user-id user) ERR_USER_ID_NOT_FOUND)) + (blockStats (unwrap! (get-mining-stats-at-block minerBlockHeight) ERR_NO_MINERS_AT_BLOCK)) + (minerStats (unwrap! (get-miner-at-block minerBlockHeight userId) ERR_USER_DID_NOT_MINE_IN_BLOCK)) + (isMature (asserts! (> stacksHeight maturityHeight) ERR_CLAIMED_BEFORE_MATURITY)) + (vrfSample (unwrap! (contract-call? 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-vrf-v2 get-save-rnd maturityHeight) ERR_NO_VRF_SEED_FOUND)) + (commitTotal (get-last-high-value-at-block minerBlockHeight)) + (winningValue (mod vrfSample commitTotal)) + ) + (asserts! (not (get rewardClaimed blockStats)) ERR_REWARD_ALREADY_CLAIMED) + (asserts! (and (>= winningValue (get lowValue minerStats)) (<= winningValue (get highValue minerStats))) + ERR_MINER_DID_NOT_WIN) + (try! (set-mining-reward-claimed userId minerBlockHeight)) + (ok true) + ) +) + +(define-private (set-mining-reward-claimed (userId uint) (minerBlockHeight uint)) + (let + ( + (blockStats (get-mining-stats-at-block-or-default minerBlockHeight)) + (minerStats (get-miner-at-block-or-default minerBlockHeight userId)) + (user (unwrap! (get-user userId) ERR_USER_NOT_FOUND)) + ) + (map-set MiningStatsAtBlock + minerBlockHeight + { + minersCount: (get minersCount blockStats), + amount: (get amount blockStats), + amountToCity: (get amountToCity blockStats), + amountToStackers: (get amountToStackers blockStats), + rewardClaimed: true + } + ) + (map-set MinersAtBlock + { + stacksHeight: minerBlockHeight, + userId: userId + } + { + ustx: (get ustx minerStats), + lowValue: (get lowValue minerStats), + highValue: (get highValue minerStats), + winner: true + } + ) + (map-set BlockWinnerIds + minerBlockHeight + userId + ) + (try! (mint-coinbase user minerBlockHeight)) + (ok true) + ) +) + +(define-read-only (is-block-winner (user principal) (minerBlockHeight uint)) + (is-block-winner-and-can-claim user minerBlockHeight false) +) + +(define-read-only (can-claim-mining-reward (user principal) (minerBlockHeight uint)) + (is-block-winner-and-can-claim user minerBlockHeight true) +) + +(define-private (is-block-winner-and-can-claim (user principal) (minerBlockHeight uint) (testCanClaim bool)) + (let + ( + (userId (unwrap! (get-user-id user) false)) + (blockStats (unwrap! (get-mining-stats-at-block minerBlockHeight) false)) + (minerStats (unwrap! (get-miner-at-block minerBlockHeight userId) false)) + (maturityHeight (+ (var-get tokenRewardMaturity) minerBlockHeight)) + (vrfSample (unwrap! (contract-call? 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-vrf-v2 get-rnd maturityHeight) false)) + (commitTotal (get-last-high-value-at-block minerBlockHeight)) + (winningValue (mod vrfSample commitTotal)) + ) + (if (and (>= winningValue (get lowValue minerStats)) (<= winningValue (get highValue minerStats))) + (if testCanClaim (not (get rewardClaimed blockStats)) true) + false + ) + ) +) + +;; STACKING CONFIGURATION + +(define-constant MAX_REWARD_CYCLES u32) +(define-constant REWARD_CYCLE_INDEXES (list u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11 u12 u13 u14 u15 u16 u17 u18 u19 u20 u21 u22 u23 u24 u25 u26 u27 u28 u29 u30 u31)) + +;; how long a reward cycle is +(define-data-var rewardCycleLength uint u2100) + +;; At a given reward cycle: +;; - how many Stackers were there +;; - what is the total uSTX submitted by miners +;; - what is the total amount of tokens stacked +(define-map StackingStatsAtCycle + uint + { + amountUstx: uint, + amountToken: uint + } +) + +;; returns the total stacked tokens and committed uSTX for a given reward cycle +(define-read-only (get-stacking-stats-at-cycle (rewardCycle uint)) + (map-get? StackingStatsAtCycle rewardCycle) +) + +;; returns the total stacked tokens and committed uSTX for a given reward cycle +;; or, an empty structure +(define-read-only (get-stacking-stats-at-cycle-or-default (rewardCycle uint)) + (default-to { amountUstx: u0, amountToken: u0 } + (map-get? StackingStatsAtCycle rewardCycle)) +) + +;; At a given reward cycle and user ID: +;; - what is the total tokens Stacked? +;; - how many tokens should be returned? (based on Stacking period) +(define-map StackerAtCycle + { + rewardCycle: uint, + userId: uint + } + { + amountStacked: uint, + toReturn: uint + } +) + +(define-read-only (get-stacker-at-cycle (rewardCycle uint) (userId uint)) + (map-get? StackerAtCycle { rewardCycle: rewardCycle, userId: userId }) +) + +(define-read-only (get-stacker-at-cycle-or-default (rewardCycle uint) (userId uint)) + (default-to { amountStacked: u0, toReturn: u0 } + (map-get? StackerAtCycle { rewardCycle: rewardCycle, userId: userId })) +) + +;; get the reward cycle for a given Stacks block height +(define-read-only (get-reward-cycle (stacksHeight uint)) + (let + ( + (firstStackingBlock (var-get activationBlock)) + (rcLen (var-get rewardCycleLength)) + ) + (if (>= stacksHeight firstStackingBlock) + (some (/ (- stacksHeight firstStackingBlock) rcLen)) + none) + ) +) + +;; determine if stacking is active in a given cycle +(define-read-only (stacking-active-at-cycle (rewardCycle uint)) + (is-some + (get amountToken (map-get? StackingStatsAtCycle rewardCycle)) + ) +) + +;; get the first Stacks block height for a given reward cycle. +(define-read-only (get-first-stacks-block-in-reward-cycle (rewardCycle uint)) + (+ (var-get activationBlock) (* (var-get rewardCycleLength) rewardCycle)) +) + +;; getter for get-entitled-stacking-reward that specifies block height +(define-read-only (get-stacking-reward (userId uint) (targetCycle uint)) + (get-entitled-stacking-reward userId targetCycle block-height) +) + +;; get uSTX a Stacker can claim, given reward cycle they stacked in and current block height +;; this method only returns a positive value if: +;; - the current block height is in a subsequent reward cycle +;; - the stacker actually locked up tokens in the target reward cycle +;; - the stacker locked up _enough_ tokens to get at least one uSTX +;; it is possible to Stack tokens and not receive uSTX: +;; - if no miners commit during this reward cycle +;; - the amount stacked by user is too few that you'd be entitled to less than 1 uSTX +(define-private (get-entitled-stacking-reward (userId uint) (targetCycle uint) (stacksHeight uint)) + (let + ( + (rewardCycleStats (get-stacking-stats-at-cycle-or-default targetCycle)) + (stackerAtCycle (get-stacker-at-cycle-or-default targetCycle userId)) + (totalUstxThisCycle (get amountUstx rewardCycleStats)) + (totalStackedThisCycle (get amountToken rewardCycleStats)) + (userStackedThisCycle (get amountStacked stackerAtCycle)) + ) + (match (get-reward-cycle stacksHeight) + currentCycle + (if (and (not (var-get isShutdown)) + (or (<= currentCycle targetCycle) (is-eq u0 userStackedThisCycle))) + ;; the contract is not shut down and + ;; this cycle hasn't finished + ;; or stacker contributed nothing + u0 + ;; (totalUstxThisCycle * userStackedThisCycle) / totalStackedThisCycle + (/ (* totalUstxThisCycle userStackedThisCycle) totalStackedThisCycle) + ) + ;; before first reward cycle + u0 + ) + ) +) + +;; STACKING ACTIONS + +(define-public (stack-tokens (amountTokens uint) (lockPeriod uint)) + (let + ( + (userId (get-or-create-user-id tx-sender)) + ) + (try! (stack-tokens-at-cycle tx-sender userId amountTokens block-height lockPeriod)) + (ok true) + ) +) + +(define-private (stack-tokens-at-cycle (user principal) (userId uint) (amountTokens uint) (startHeight uint) (lockPeriod uint)) + (let + ( + (currentCycle (unwrap! (get-reward-cycle startHeight) ERR_STACKING_NOT_AVAILABLE)) + (targetCycle (+ u1 currentCycle)) + (commitment { + stackerId: userId, + amount: amountTokens, + first: targetCycle, + last: (+ targetCycle lockPeriod) + }) + ) + (asserts! (is-activated) ERR_CONTRACT_NOT_ACTIVATED) + (asserts! (and (> lockPeriod u0) (<= lockPeriod MAX_REWARD_CYCLES)) + ERR_CANNOT_STACK) + (asserts! (> amountTokens u0) ERR_CANNOT_STACK) + (try! (contract-call? 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2 transfer amountTokens tx-sender (as-contract tx-sender) none)) + (print { + firstCycle: targetCycle, + lastCycle: (- (+ targetCycle lockPeriod) u1) + }) + (match (fold stack-tokens-closure REWARD_CYCLE_INDEXES (ok commitment)) + okValue (ok true) + errValue (err errValue) + ) + ) +) + +(define-private (stack-tokens-closure (rewardCycleIdx uint) + (commitmentResponse (response + { + stackerId: uint, + amount: uint, + first: uint, + last: uint + } + uint + ))) + + (match commitmentResponse + commitment + (let + ( + (stackerId (get stackerId commitment)) + (amountToken (get amount commitment)) + (firstCycle (get first commitment)) + (lastCycle (get last commitment)) + (targetCycle (+ firstCycle rewardCycleIdx)) + ) + (begin + (if (and (>= targetCycle firstCycle) (< targetCycle lastCycle)) + (begin + (if (is-eq targetCycle (- lastCycle u1)) + (set-tokens-stacked stackerId targetCycle amountToken amountToken) + (set-tokens-stacked stackerId targetCycle amountToken u0) + ) + true + ) + false + ) + commitmentResponse + ) + ) + errValue commitmentResponse + ) +) + +(define-private (set-tokens-stacked (userId uint) (targetCycle uint) (amountStacked uint) (toReturn uint)) + (let + ( + (rewardCycleStats (get-stacking-stats-at-cycle-or-default targetCycle)) + (stackerAtCycle (get-stacker-at-cycle-or-default targetCycle userId)) + ) + (map-set StackingStatsAtCycle + targetCycle + { + amountUstx: (get amountUstx rewardCycleStats), + amountToken: (+ amountStacked (get amountToken rewardCycleStats)) + } + ) + (map-set StackerAtCycle + { + rewardCycle: targetCycle, + userId: userId + } + { + amountStacked: (+ amountStacked (get amountStacked stackerAtCycle)), + toReturn: (+ toReturn (get toReturn stackerAtCycle)) + } + ) + ) +) + +;; STACKING REWARD CLAIMS + +;; calls function to claim stacking reward in active logic contract +(define-public (claim-stacking-reward (targetCycle uint)) + (begin + (try! (claim-stacking-reward-at-cycle tx-sender block-height targetCycle)) + (ok true) + ) +) + +(define-private (claim-stacking-reward-at-cycle (user principal) (stacksHeight uint) (targetCycle uint)) + (let + ( + (currentCycle (unwrap! (get-reward-cycle stacksHeight) ERR_STACKING_NOT_AVAILABLE)) + (userId (unwrap! (get-user-id user) ERR_USER_ID_NOT_FOUND)) + (entitledUstx (get-entitled-stacking-reward userId targetCycle stacksHeight)) + (stackerAtCycle (get-stacker-at-cycle-or-default targetCycle userId)) + (toReturn (get toReturn stackerAtCycle)) + ) + (asserts! (or + (is-eq true (var-get isShutdown)) + (> currentCycle targetCycle)) + ERR_REWARD_CYCLE_NOT_COMPLETED) + (asserts! (or (> toReturn u0) (> entitledUstx u0)) ERR_NOTHING_TO_REDEEM) + ;; disable ability to claim again + (map-set StackerAtCycle + { + rewardCycle: targetCycle, + userId: userId + } + { + amountStacked: u0, + toReturn: u0 + } + ) + ;; send back tokens if user was eligible + (if (> toReturn u0) + (try! (as-contract (contract-call? 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2 transfer toReturn tx-sender user none))) + true + ) + ;; send back rewards if user was eligible + (if (> entitledUstx u0) + (try! (as-contract (stx-transfer? entitledUstx tx-sender user))) + true + ) + (ok true) + ) +) + +;; TOKEN CONFIGURATION + +;; decimals and multiplier for token +(define-constant DECIMALS u6) +(define-constant MICRO_CITYCOINS (pow u10 DECIMALS)) + +;; bonus period length for increased coinbase rewards +(define-constant TOKEN_BONUS_PERIOD u10000) + +;; coinbase thresholds per halving, used to determine halvings +(define-data-var coinbaseThreshold1 uint u0) +(define-data-var coinbaseThreshold2 uint u0) +(define-data-var coinbaseThreshold3 uint u0) +(define-data-var coinbaseThreshold4 uint u0) +(define-data-var coinbaseThreshold5 uint u0) + +;; return coinbase thresholds if contract activated +(define-read-only (get-coinbase-thresholds) + (let + ( + (activated (get-activation-status)) + ) + (asserts! activated ERR_CONTRACT_NOT_ACTIVATED) + (ok { + coinbaseThreshold1: (var-get coinbaseThreshold1), + coinbaseThreshold2: (var-get coinbaseThreshold2), + coinbaseThreshold3: (var-get coinbaseThreshold3), + coinbaseThreshold4: (var-get coinbaseThreshold4), + coinbaseThreshold5: (var-get coinbaseThreshold5) + }) + ) +) + +;; set coinbase thresholds, used during activation +(define-private (set-coinbase-thresholds) + (let + ( + (coinbaseThresholds (try! (contract-call? 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2 get-coinbase-thresholds))) + ) + (var-set coinbaseThreshold1 (get coinbaseThreshold1 coinbaseThresholds)) + (var-set coinbaseThreshold2 (get coinbaseThreshold2 coinbaseThresholds)) + (var-set coinbaseThreshold3 (get coinbaseThreshold3 coinbaseThresholds)) + (var-set coinbaseThreshold4 (get coinbaseThreshold4 coinbaseThresholds)) + (var-set coinbaseThreshold5 (get coinbaseThreshold5 coinbaseThresholds)) + ;; print coinbase thresholds + (print { + coinbaseThreshold1: (var-get coinbaseThreshold1), + coinbaseThreshold2: (var-get coinbaseThreshold2), + coinbaseThreshold3: (var-get coinbaseThreshold3), + coinbaseThreshold4: (var-get coinbaseThreshold4), + coinbaseThreshold5: (var-get coinbaseThreshold5) + }) + (ok true) + ) +) + +;; guarded function for auth to update coinbase thresholds +(define-public (update-coinbase-thresholds) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (try! (set-coinbase-thresholds)) + (ok true) + ) +) + +;; coinbase rewards per threshold, used to determine rewards +(define-data-var coinbaseAmountBonus uint u0) +(define-data-var coinbaseAmount1 uint u0) +(define-data-var coinbaseAmount2 uint u0) +(define-data-var coinbaseAmount3 uint u0) +(define-data-var coinbaseAmount4 uint u0) +(define-data-var coinbaseAmount5 uint u0) +(define-data-var coinbaseAmountDefault uint u0) + +;; return coinbase amounts if contract activated +(define-read-only (get-coinbase-amounts) + (let + ( + (activated (get-activation-status)) + ) + (asserts! activated ERR_CONTRACT_NOT_ACTIVATED) + (ok { + coinbaseAmountBonus: (var-get coinbaseAmountBonus), + coinbaseAmount1: (var-get coinbaseAmount1), + coinbaseAmount2: (var-get coinbaseAmount2), + coinbaseAmount3: (var-get coinbaseAmount3), + coinbaseAmount4: (var-get coinbaseAmount4), + coinbaseAmount5: (var-get coinbaseAmount5), + coinbaseAmountDefault: (var-get coinbaseAmountDefault) + }) + ) +) + +;; set coinbase amounts, used during activation +(define-private (set-coinbase-amounts) + (let + ( + (coinbaseAmounts (unwrap! (contract-call? 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2 get-coinbase-amounts) ERR_COINBASE_AMOUNTS_NOT_FOUND)) + ) + (var-set coinbaseAmountBonus (get coinbaseAmountBonus coinbaseAmounts)) + (var-set coinbaseAmount1 (get coinbaseAmount1 coinbaseAmounts)) + (var-set coinbaseAmount2 (get coinbaseAmount2 coinbaseAmounts)) + (var-set coinbaseAmount3 (get coinbaseAmount3 coinbaseAmounts)) + (var-set coinbaseAmount4 (get coinbaseAmount4 coinbaseAmounts)) + (var-set coinbaseAmount5 (get coinbaseAmount5 coinbaseAmounts)) + (var-set coinbaseAmountDefault (get coinbaseAmountDefault coinbaseAmounts)) + ;; print coinbase amounts + (print { + coinbaseAmountBonus: (var-get coinbaseAmountBonus), + coinbaseAmount1: (var-get coinbaseAmount1), + coinbaseAmount2: (var-get coinbaseAmount2), + coinbaseAmount3: (var-get coinbaseAmount3), + coinbaseAmount4: (var-get coinbaseAmount4), + coinbaseAmount5: (var-get coinbaseAmount5), + coinbaseAmountDefault: (var-get coinbaseAmountDefault) + }) + (ok true) + ) +) + +;; guarded function for auth to update coinbase amounts +(define-public (update-coinbase-amounts) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (try! (set-coinbase-amounts)) + (ok true) + ) +) + +;; function for deciding how many tokens to mint, depending on when they were mined +(define-read-only (get-coinbase-amount (minerBlockHeight uint)) + (begin + ;; if contract is not active, return 0 + (asserts! (>= minerBlockHeight (var-get activationBlock)) u0) + ;; if contract is active, return based on emissions schedule + ;; defined in CCIP-008 https://github.com/citycoins/governance + (asserts! (> minerBlockHeight (var-get coinbaseThreshold1)) + (if (<= (- minerBlockHeight (var-get activationBlock)) TOKEN_BONUS_PERIOD) + ;; bonus reward for initial miners + (var-get coinbaseAmountBonus) + ;; standard reward until 1st halving + (var-get coinbaseAmount1) + ) + ) + ;; computations based on each halving threshold + (asserts! (> minerBlockHeight (var-get coinbaseThreshold2)) (var-get coinbaseAmount2)) + (asserts! (> minerBlockHeight (var-get coinbaseThreshold3)) (var-get coinbaseAmount3)) + (asserts! (> minerBlockHeight (var-get coinbaseThreshold4)) (var-get coinbaseAmount4)) + (asserts! (> minerBlockHeight (var-get coinbaseThreshold5)) (var-get coinbaseAmount5)) + ;; default value after 5th halving + (var-get coinbaseAmountDefault) + ) +) + +;; mint new tokens for claimant who won at given Stacks block height +(define-private (mint-coinbase (recipient principal) (stacksHeight uint)) + (as-contract (contract-call? 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2 mint (get-coinbase-amount stacksHeight) recipient)) +) + +;; UTILITIES + +(define-data-var shutdownHeight uint u0) +(define-data-var isShutdown bool false) + +;; stop mining and stacking operations +;; in preparation for a core upgrade +(define-public (shutdown-contract (stacksHeight uint)) + (begin + ;; make sure block height is in the future + (asserts! (>= stacksHeight block-height) ERR_BLOCK_HEIGHT_IN_PAST) + ;; only allow shutdown request from AUTH + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + ;; set variables to disable mining/stacking in CORE + (var-set activationReached false) + (var-set shutdownHeight stacksHeight) + ;; set variable to allow for all stacking claims + (var-set isShutdown true) + (ok true) + ) +) + +;; checks if caller is Auth contract +(define-private (is-authorized-auth) + (is-eq contract-caller 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-auth-v2) +) + +;; checks if contract is fully activated to +;; enable mining and stacking functions +(define-private (is-activated) + (and (get-activation-status) (>= block-height (var-get activationTarget))) +) diff --git a/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v2.json b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v2.json new file mode 100644 index 0000000..89c8a70 --- /dev/null +++ b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v2.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2.clar b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2.clar new file mode 100644 index 0000000..da70bb8 --- /dev/null +++ b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2.clar @@ -0,0 +1,301 @@ +;; MIAMICOIN TOKEN V2 CONTRACT +;; CityCoins Protocol Version 2.0.0 + +;; TRAIT DEFINITIONS + +(impl-trait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-token-trait.citycoin-token) +(impl-trait 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-token-v2-trait.citycoin-token-v2) +(use-trait coreTrait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.citycoin-core) + +;; ERROR CODES + +(define-constant ERR_UNAUTHORIZED (err u2000)) +(define-constant ERR_TOKEN_NOT_ACTIVATED (err u2001)) +(define-constant ERR_TOKEN_ALREADY_ACTIVATED (err u2002)) +(define-constant ERR_V1_BALANCE_NOT_FOUND (err u2003)) +(define-constant ERR_INVALID_COINBASE_THRESHOLD (err u2004)) +(define-constant ERR_INVALID_COINBASE_AMOUNT (err u2005)) + +;; SIP-010 DEFINITION + +(impl-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait) + +(define-fungible-token miamicoin) + +(define-constant DECIMALS u6) +(define-constant MICRO_CITYCOINS (pow u10 DECIMALS)) + +;; SIP-010 FUNCTIONS + +(define-public (transfer (amount uint) (from principal) (to principal) (memo (optional (buff 34)))) + (begin + (asserts! (is-eq from tx-sender) ERR_UNAUTHORIZED) + (if (is-some memo) + (print memo) + none + ) + (ft-transfer? miamicoin amount from to) + ) +) + +(define-read-only (get-name) + (ok "miamicoin") +) + +(define-read-only (get-symbol) + (ok "MIA") +) + +(define-read-only (get-decimals) + (ok DECIMALS) +) + +(define-read-only (get-balance (user principal)) + (ok (ft-get-balance miamicoin user)) +) + +(define-read-only (get-total-supply) + (ok (ft-get-supply miamicoin)) +) + +(define-read-only (get-token-uri) + (ok (var-get tokenUri)) +) + +;; TOKEN CONFIGURATION + +;; define bonus period and initial epoch length +(define-constant TOKEN_BONUS_PERIOD u10000) +(define-constant TOKEN_EPOCH_LENGTH u25000) + +;; once activated, activation cannot happen again +(define-data-var tokenActivated bool false) + +;; core contract states +(define-constant STATE_DEPLOYED u0) +(define-constant STATE_ACTIVE u1) +(define-constant STATE_INACTIVE u2) + +;; one-time function to activate the token +(define-public (activate-token (coreContract principal) (stacksHeight uint)) + (let + ( + (coreContractMap (try! (contract-call? 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-auth-v2 get-core-contract-info coreContract))) + (threshold1 (+ stacksHeight TOKEN_BONUS_PERIOD TOKEN_EPOCH_LENGTH)) ;; 35,000 blocks + (threshold2 (+ stacksHeight TOKEN_BONUS_PERIOD (* u3 TOKEN_EPOCH_LENGTH))) ;; 85,000 blocks + (threshold3 (+ stacksHeight TOKEN_BONUS_PERIOD (* u7 TOKEN_EPOCH_LENGTH))) ;; 185,000 blocks + (threshold4 (+ stacksHeight TOKEN_BONUS_PERIOD (* u15 TOKEN_EPOCH_LENGTH))) ;; 385,000 blocks + (threshold5 (+ stacksHeight TOKEN_BONUS_PERIOD (* u31 TOKEN_EPOCH_LENGTH))) ;; 785,000 blocks + ) + (asserts! (is-eq (get state coreContractMap) STATE_ACTIVE) ERR_UNAUTHORIZED) + (asserts! (not (var-get tokenActivated)) ERR_TOKEN_ALREADY_ACTIVATED) + (var-set tokenActivated true) + (try! (set-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5)) + (ok true) + ) +) + +;; COINBASE THRESHOLDS + +;; coinbase thresholds per halving, used to select coinbase rewards in core +;; initially set by register-user in core contract per CCIP-008 +(define-data-var coinbaseThreshold1 uint u0) +(define-data-var coinbaseThreshold2 uint u0) +(define-data-var coinbaseThreshold3 uint u0) +(define-data-var coinbaseThreshold4 uint u0) +(define-data-var coinbaseThreshold5 uint u0) + +;; return coinbase thresholds if token activated +(define-read-only (get-coinbase-thresholds) + (let + ( + (activated (var-get tokenActivated)) + ) + (asserts! activated ERR_TOKEN_NOT_ACTIVATED) + (ok { + coinbaseThreshold1: (var-get coinbaseThreshold1), + coinbaseThreshold2: (var-get coinbaseThreshold2), + coinbaseThreshold3: (var-get coinbaseThreshold3), + coinbaseThreshold4: (var-get coinbaseThreshold4), + coinbaseThreshold5: (var-get coinbaseThreshold5) + }) + ) +) + +(define-private (set-coinbase-thresholds (threshold1 uint) (threshold2 uint) (threshold3 uint) (threshold4 uint) (threshold5 uint)) + (begin + ;; check that all thresholds increase in value + (asserts! (and (> threshold1 u0) (> threshold2 threshold1) (> threshold3 threshold2) (> threshold4 threshold3) (> threshold5 threshold4)) ERR_INVALID_COINBASE_THRESHOLD) + ;; set coinbase thresholds + (var-set coinbaseThreshold1 threshold1) + (var-set coinbaseThreshold2 threshold2) + (var-set coinbaseThreshold3 threshold3) + (var-set coinbaseThreshold4 threshold4) + (var-set coinbaseThreshold5 threshold5) + ;; print coinbase thresholds + (print { + coinbaseThreshold1: threshold1, + coinbaseThreshold2: threshold2, + coinbaseThreshold3: threshold3, + coinbaseThreshold4: threshold4, + coinbaseThreshold5: threshold5 + }) + (ok true) + ) +) + +;; only accessible by auth +(define-public (update-coinbase-thresholds (threshold1 uint) (threshold2 uint) (threshold3 uint) (threshold4 uint) (threshold5 uint)) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (asserts! (var-get tokenActivated) ERR_TOKEN_NOT_ACTIVATED) + (try! (set-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5)) + (ok true) + ) +) + +;; COINBASE AMOUNTS (REWARDS) + +;; coinbase rewards per threshold per CCIP-008 +(define-data-var coinbaseAmountBonus uint (* MICRO_CITYCOINS u250000)) +(define-data-var coinbaseAmount1 uint (* MICRO_CITYCOINS u100000)) +(define-data-var coinbaseAmount2 uint (* MICRO_CITYCOINS u50000)) +(define-data-var coinbaseAmount3 uint (* MICRO_CITYCOINS u25000)) +(define-data-var coinbaseAmount4 uint (* MICRO_CITYCOINS u12500)) +(define-data-var coinbaseAmount5 uint (* MICRO_CITYCOINS u6250)) +(define-data-var coinbaseAmountDefault uint (* MICRO_CITYCOINS u3125)) + +;; return coinbase thresholds if token activated +(define-read-only (get-coinbase-amounts) + (ok { + coinbaseAmountBonus: (var-get coinbaseAmountBonus), + coinbaseAmount1: (var-get coinbaseAmount1), + coinbaseAmount2: (var-get coinbaseAmount2), + coinbaseAmount3: (var-get coinbaseAmount3), + coinbaseAmount4: (var-get coinbaseAmount4), + coinbaseAmount5: (var-get coinbaseAmount5), + coinbaseAmountDefault: (var-get coinbaseAmountDefault) + }) +) + +(define-private (set-coinbase-amounts (amountBonus uint) (amount1 uint) (amount2 uint) (amount3 uint) (amount4 uint) (amount5 uint) (amountDefault uint)) + (begin + ;; check that all amounts are greater than zero + (asserts! (and (> amountBonus u0) (> amount1 u0) (> amount2 u0) (> amount3 u0) (> amount4 u0) (> amount5 u0) (> amountDefault u0)) ERR_INVALID_COINBASE_AMOUNT) + ;; set coinbase amounts in token contract + (var-set coinbaseAmountBonus amountBonus) + (var-set coinbaseAmount1 amount1) + (var-set coinbaseAmount2 amount2) + (var-set coinbaseAmount3 amount3) + (var-set coinbaseAmount4 amount4) + (var-set coinbaseAmount5 amount5) + (var-set coinbaseAmountDefault amountDefault) + ;; print coinbase amounts + (print { + coinbaseAmountBonus: amountBonus, + coinbaseAmount1: amount1, + coinbaseAmount2: amount2, + coinbaseAmount3: amount3, + coinbaseAmount4: amount4, + coinbaseAmount5: amount5, + coinbaseAmountDefault: amountDefault + }) + (ok true) + ) +) + +;; only accessible by auth +(define-public (update-coinbase-amounts (amountBonus uint) (amount1 uint) (amount2 uint) (amount3 uint) (amount4 uint) (amount5 uint) (amountDefault uint)) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + ;; (asserts! (var-get tokenActivated) ERR_TOKEN_NOT_ACTIVATED) + (try! (set-coinbase-amounts amountBonus amount1 amount2 amount3 amount4 amount5 amountDefault)) + (ok true) + ) +) + +;; V1 TO V2 CONVERSION + +(define-public (convert-to-v2) + (let + ( + (balanceV1 (unwrap! (contract-call? 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-token get-balance tx-sender) ERR_V1_BALANCE_NOT_FOUND)) + ) + ;; verify positive balance + (asserts! (> balanceV1 u0) ERR_V1_BALANCE_NOT_FOUND) + ;; burn old + (try! (contract-call? 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v1-patch burn-mia-v1 balanceV1 tx-sender)) + (print { + burnedV1: balanceV1, + mintedV2: (* balanceV1 MICRO_CITYCOINS), + tx-sender: tx-sender, + contract-caller: contract-caller + }) + ;; create new + (ft-mint? miamicoin (* balanceV1 MICRO_CITYCOINS) tx-sender) + ) +) + +;; UTILITIES + +(define-data-var tokenUri (optional (string-utf8 256)) (some u"https://cdn.citycoins.co/metadata/miamicoin.json")) + +;; set token URI to new value, only accessible by Auth +(define-public (set-token-uri (newUri (optional (string-utf8 256)))) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (ok (var-set tokenUri newUri)) + ) +) + +;; mint new tokens, only accessible by a Core contract +(define-public (mint (amount uint) (recipient principal)) + (let + ( + (coreContract (try! (contract-call? 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-auth-v2 get-core-contract-info contract-caller))) + ) + (ft-mint? miamicoin amount recipient) + ) +) + +;; burn tokens +(define-public (burn (amount uint) (owner principal)) + (begin + (asserts! (is-eq tx-sender owner) ERR_UNAUTHORIZED) + (ft-burn? miamicoin amount owner) + ) +) + +;; checks if caller is Auth contract +(define-private (is-authorized-auth) + (is-eq contract-caller 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-auth-v2) +) + +;; SEND-MANY + +(define-public (send-many (recipients (list 200 { to: principal, amount: uint, memo: (optional (buff 34)) }))) + (fold check-err + (map send-token recipients) + (ok true) + ) +) + +(define-private (check-err (result (response bool uint)) (prior (response bool uint))) + (match prior ok-value + result + err-value (err err-value) + ) +) + +(define-private (send-token (recipient { to: principal, amount: uint, memo: (optional (buff 34)) })) + (send-token-with-memo (get amount recipient) (get to recipient) (get memo recipient)) +) + +(define-private (send-token-with-memo (amount uint) (to principal) (memo (optional (buff 34)))) + (let + ( + (transferOk (try! (transfer amount tx-sender to memo))) + ) + (ok transferOk) + ) +) diff --git a/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2.json b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2.json new file mode 100644 index 0000000..89c8a70 --- /dev/null +++ b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-auth.clar b/.cache/requirements/SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-auth.clar new file mode 100644 index 0000000..9d2790d --- /dev/null +++ b/.cache/requirements/SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-auth.clar @@ -0,0 +1,564 @@ +;; NEWYORKCITYCOIN AUTH CONTRACT +;; CityCoins Protocol Version 1.0.1 + +(define-constant CONTRACT_OWNER tx-sender) + +;; TRAIT DEFINITIONS + +(use-trait coreTrait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.citycoin-core) +(use-trait tokenTrait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-token-trait.citycoin-token) + +;; ERRORS + +(define-constant ERR_UNKNOWN_JOB u6000) +(define-constant ERR_UNAUTHORIZED u6001) +(define-constant ERR_JOB_IS_ACTIVE u6002) +(define-constant ERR_JOB_IS_NOT_ACTIVE u6003) +(define-constant ERR_ALREADY_VOTED_THIS_WAY u6004) +(define-constant ERR_JOB_IS_EXECUTED u6005) +(define-constant ERR_JOB_IS_NOT_APPROVED u6006) +(define-constant ERR_ARGUMENT_ALREADY_EXISTS u6007) +(define-constant ERR_NO_ACTIVE_CORE_CONTRACT u6008) +(define-constant ERR_CORE_CONTRACT_NOT_FOUND u6009) +(define-constant ERR_UNKNOWN_ARGUMENT u6010) + +;; JOB MANAGEMENT + +(define-constant REQUIRED_APPROVALS u3) + +(define-data-var lastJobId uint u0) + +(define-map Jobs + uint + { + creator: principal, + name: (string-ascii 255), + target: principal, + approvals: uint, + disapprovals: uint, + isActive: bool, + isExecuted: bool + } +) + +(define-map JobApprovers + { jobId: uint, approver: principal } + bool +) + +(define-map Approvers + principal + bool +) + +(define-map ArgumentLastIdsByType + { jobId: uint, argumentType: (string-ascii 25) } + uint +) + +(define-map UIntArgumentsByName + { jobId: uint, argumentName: (string-ascii 255) } + { argumentId: uint, value: uint} +) + +(define-map UIntArgumentsById + { jobId: uint, argumentId: uint } + { argumentName: (string-ascii 255), value: uint } +) + +(define-map PrincipalArgumentsByName + { jobId: uint, argumentName: (string-ascii 255) } + { argumentId: uint, value: principal } +) + +(define-map PrincipalArgumentsById + { jobId: uint, argumentId: uint } + { argumentName: (string-ascii 255), value: principal } +) + +;; FUNCTIONS + +(define-read-only (get-last-job-id) + (var-get lastJobId) +) + +(define-public (create-job (name (string-ascii 255)) (target principal)) + (let + ( + (newJobId (+ (var-get lastJobId) u1)) + ) + (asserts! (is-approver tx-sender) (err ERR_UNAUTHORIZED)) + (map-set Jobs + newJobId + { + creator: tx-sender, + name: name, + target: target, + approvals: u0, + disapprovals: u0, + isActive: false, + isExecuted: false + } + ) + (var-set lastJobId newJobId) + (ok newJobId) + ) +) + +(define-read-only (get-job (jobId uint)) + (map-get? Jobs jobId) +) + +(define-public (activate-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) (err ERR_UNKNOWN_JOB))) + ) + (asserts! (is-eq (get creator job) tx-sender) (err ERR_UNAUTHORIZED)) + (asserts! (not (get isActive job)) (err ERR_JOB_IS_ACTIVE)) + (map-set Jobs + jobId + (merge job { isActive: true }) + ) + (ok true) + ) +) + +(define-public (approve-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) (err ERR_UNKNOWN_JOB))) + (previousVote (map-get? JobApprovers { jobId: jobId, approver: tx-sender })) + ) + (asserts! (get isActive job) (err ERR_JOB_IS_NOT_ACTIVE)) + (asserts! (is-approver tx-sender) (err ERR_UNAUTHORIZED)) + ;; save vote + (map-set JobApprovers + { jobId: jobId, approver: tx-sender } + true + ) + (match previousVote approved + (begin + (asserts! (not approved) (err ERR_ALREADY_VOTED_THIS_WAY)) + (map-set Jobs jobId + (merge job + { + approvals: (+ (get approvals job) u1), + disapprovals: (- (get disapprovals job) u1) + } + ) + ) + ) + ;; no previous vote + (map-set Jobs + jobId + (merge job { approvals: (+ (get approvals job) u1) } ) + ) + ) + (ok true) + ) +) + +(define-public (disapprove-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) (err ERR_UNKNOWN_JOB))) + (previousVote (map-get? JobApprovers { jobId: jobId, approver: tx-sender })) + ) + (asserts! (get isActive job) (err ERR_JOB_IS_NOT_ACTIVE)) + (asserts! (is-approver tx-sender) (err ERR_UNAUTHORIZED)) + ;; save vote + (map-set JobApprovers + { jobId: jobId, approver: tx-sender } + false + ) + (match previousVote approved + (begin + (asserts! approved (err ERR_ALREADY_VOTED_THIS_WAY)) + (map-set Jobs jobId + (merge job + { + approvals: (- (get approvals job) u1), + disapprovals: (+ (get disapprovals job) u1) + } + ) + ) + ) + ;; no previous vote + (map-set Jobs + jobId + (merge job { disapprovals: (+ (get disapprovals job) u1) } ) + ) + ) + (ok true) + ) +) + +(define-read-only (is-job-approved (jobId uint)) + (match (get-job jobId) job + (>= (get approvals job) REQUIRED_APPROVALS) + false + ) +) + +(define-public (mark-job-as-executed (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) (err ERR_UNKNOWN_JOB))) + ) + (asserts! (get isActive job) (err ERR_JOB_IS_NOT_ACTIVE)) + (asserts! (>= (get approvals job) REQUIRED_APPROVALS) (err ERR_JOB_IS_NOT_APPROVED)) + (asserts! (is-eq (get target job) contract-caller) (err ERR_UNAUTHORIZED)) + (asserts! (not (get isExecuted job)) (err ERR_JOB_IS_EXECUTED)) + (map-set Jobs + jobId + (merge job { isExecuted: true }) + ) + (ok true) + ) +) + +(define-public (add-uint-argument (jobId uint) (argumentName (string-ascii 255)) (value uint)) + (let + ( + (argumentId (generate-argument-id jobId "uint")) + ) + (try! (guard-add-argument jobId)) + (asserts! + (and + (map-insert UIntArgumentsById + { jobId: jobId, argumentId: argumentId } + { argumentName: argumentName, value: value } + ) + (map-insert UIntArgumentsByName + { jobId: jobId, argumentName: argumentName } + { argumentId: argumentId, value: value} + ) + ) + (err ERR_ARGUMENT_ALREADY_EXISTS) + ) + (ok true) + ) +) + +(define-read-only (get-uint-argument-by-name (jobId uint) (argumentName (string-ascii 255))) + (map-get? UIntArgumentsByName { jobId: jobId, argumentName: argumentName }) +) + +(define-read-only (get-uint-argument-by-id (jobId uint) (argumentId uint)) + (map-get? UIntArgumentsById { jobId: jobId, argumentId: argumentId }) +) + +(define-read-only (get-uint-value-by-name (jobId uint) (argumentName (string-ascii 255))) + (get value (get-uint-argument-by-name jobId argumentName)) +) + +(define-read-only (get-uint-value-by-id (jobId uint) (argumentId uint)) + (get value (get-uint-argument-by-id jobId argumentId)) +) + +(define-public (add-principal-argument (jobId uint) (argumentName (string-ascii 255)) (value principal)) + (let + ( + (argumentId (generate-argument-id jobId "principal")) + ) + (try! (guard-add-argument jobId)) + (asserts! + (and + (map-insert PrincipalArgumentsById + { jobId: jobId, argumentId: argumentId } + { argumentName: argumentName, value: value } + ) + (map-insert PrincipalArgumentsByName + { jobId: jobId, argumentName: argumentName } + { argumentId: argumentId, value: value} + ) + ) + (err ERR_ARGUMENT_ALREADY_EXISTS) + ) + (ok true) + ) +) + +(define-read-only (get-principal-argument-by-name (jobId uint) (argumentName (string-ascii 255))) + (map-get? PrincipalArgumentsByName { jobId: jobId, argumentName: argumentName }) +) + +(define-read-only (get-principal-argument-by-id (jobId uint) (argumentId uint)) + (map-get? PrincipalArgumentsById { jobId: jobId, argumentId: argumentId }) +) + +(define-read-only (get-principal-value-by-name (jobId uint) (argumentName (string-ascii 255))) + (get value (get-principal-argument-by-name jobId argumentName)) +) + +(define-read-only (get-principal-value-by-id (jobId uint) (argumentId uint)) + (get value (get-principal-argument-by-id jobId argumentId)) +) + +;; PRIVATE FUNCTIONS + +(define-read-only (is-approver (user principal)) + (default-to false (map-get? Approvers user)) +) + +(define-private (generate-argument-id (jobId uint) (argumentType (string-ascii 25))) + (let + ( + (argumentId (+ (default-to u0 (map-get? ArgumentLastIdsByType { jobId: jobId, argumentType: argumentType })) u1)) + ) + (map-set ArgumentLastIdsByType + { jobId: jobId, argumentType: argumentType } + argumentId + ) + ;; return + argumentId + ) +) + +(define-private (guard-add-argument (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) (err ERR_UNKNOWN_JOB))) + ) + (asserts! (not (get isActive job)) (err ERR_JOB_IS_ACTIVE)) + (asserts! (is-eq (get creator job) contract-caller) (err ERR_UNAUTHORIZED)) + (ok true) + ) +) + +;; CONTRACT MANAGEMENT + +;; initial value for active core contract +;; set to deployer address at startup to prevent +;; circular dependency of core on auth +(define-data-var activeCoreContract principal CONTRACT_OWNER) +(define-data-var initialized bool false) + +;; core contract states +(define-constant STATE_DEPLOYED u0) +(define-constant STATE_ACTIVE u1) +(define-constant STATE_INACTIVE u2) + +;; core contract map +(define-map CoreContracts + principal + { + state: uint, + startHeight: uint, + endHeight: uint + } +) + +;; getter for active core contract +(define-read-only (get-active-core-contract) + (begin + (asserts! (not (is-eq (var-get activeCoreContract) CONTRACT_OWNER)) (err ERR_NO_ACTIVE_CORE_CONTRACT)) + (ok (var-get activeCoreContract)) + ) +) + +;; getter for core contract map +(define-read-only (get-core-contract-info (targetContract principal)) + (let + ( + (coreContract (unwrap! (map-get? CoreContracts targetContract) (err ERR_CORE_CONTRACT_NOT_FOUND))) + ) + (ok coreContract) + ) +) + +;; one-time function to initialize contracts after all contracts are deployed +;; - check that deployer is calling this function +;; - check this contract is not activated already (one-time use) +;; - set initial map value for core contract v1 +;; - set cityWallet in core contract +;; - set intialized true +(define-public (initialize-contracts (coreContract )) + (let + ( + (coreContractAddress (contract-of coreContract)) + ) + (asserts! (is-eq contract-caller CONTRACT_OWNER) (err ERR_UNAUTHORIZED)) + (asserts! (not (var-get initialized)) (err ERR_UNAUTHORIZED)) + (map-set CoreContracts + coreContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (try! (contract-call? coreContract set-city-wallet (var-get cityWallet))) + (var-set initialized true) + (ok true) + ) +) + +(define-read-only (is-initialized) + (var-get initialized) +) + +;; function to activate core contract through registration +;; - check that target is in core contract map +;; - check that caller is core contract +;; - set active in core contract map +;; - set as activeCoreContract +(define-public (activate-core-contract (targetContract principal) (stacksHeight uint)) + (let + ( + (coreContract (unwrap! (map-get? CoreContracts targetContract) (err ERR_CORE_CONTRACT_NOT_FOUND))) + ) + (asserts! (is-eq contract-caller targetContract) (err ERR_UNAUTHORIZED)) + (map-set CoreContracts + targetContract + { + state: STATE_ACTIVE, + startHeight: stacksHeight, + endHeight: u0 + }) + (var-set activeCoreContract targetContract) + (ok true) + ) +) + +;; protected function to update core contract +(define-public (upgrade-core-contract (oldContract ) (newContract )) + (let + ( + (oldContractAddress (contract-of oldContract)) + (oldContractMap (unwrap! (map-get? CoreContracts oldContractAddress) (err ERR_CORE_CONTRACT_NOT_FOUND))) + (newContractAddress (contract-of newContract)) + ) + (asserts! (not (is-eq oldContractAddress newContractAddress)) (err ERR_UNAUTHORIZED)) + (asserts! (is-authorized-city) (err ERR_UNAUTHORIZED)) + (map-set CoreContracts + oldContractAddress + { + state: STATE_INACTIVE, + startHeight: (get startHeight oldContractMap), + endHeight: block-height + }) + (map-set CoreContracts + newContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (var-set activeCoreContract newContractAddress) + (try! (contract-call? oldContract shutdown-contract block-height)) + (try! (contract-call? newContract set-city-wallet (var-get cityWallet))) + (ok true) + ) +) + +(define-public (execute-upgrade-core-contract-job (jobId uint) (oldContract ) (newContract )) + (let + ( + (oldContractArg (unwrap! (get-principal-value-by-name jobId "oldContract") (err ERR_UNKNOWN_ARGUMENT))) + (newContractArg (unwrap! (get-principal-value-by-name jobId "newContract") (err ERR_UNKNOWN_ARGUMENT))) + (oldContractAddress (contract-of oldContract)) + (oldContractMap (unwrap! (map-get? CoreContracts oldContractAddress) (err ERR_CORE_CONTRACT_NOT_FOUND))) + (newContractAddress (contract-of newContract)) + ) + (asserts! (is-approver contract-caller) (err ERR_UNAUTHORIZED)) + (asserts! (and (is-eq oldContractArg oldContractAddress) (is-eq newContractArg newContractAddress)) (err ERR_UNAUTHORIZED)) + (asserts! (not (is-eq oldContractAddress newContractAddress)) (err ERR_UNAUTHORIZED)) + (map-set CoreContracts + oldContractAddress + { + state: STATE_INACTIVE, + startHeight: (get startHeight oldContractMap), + endHeight: block-height + }) + (map-set CoreContracts + newContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (var-set activeCoreContract newContractAddress) + (try! (contract-call? oldContract shutdown-contract block-height)) + (try! (contract-call? newContract set-city-wallet (var-get cityWallet))) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; CITY WALLET MANAGEMENT + +;; initial value for city wallet +(define-data-var cityWallet principal 'SM18VBF2QYAAHN57Q28E2HSM15F6078JZYZ2FQBCX) + +;; returns city wallet principal +(define-read-only (get-city-wallet) + (ok (var-get cityWallet)) +) + +;; protected function to update city wallet variable +(define-public (set-city-wallet (targetContract ) (newCityWallet principal)) + (let + ( + (coreContractAddress (contract-of targetContract)) + (coreContract (unwrap! (map-get? CoreContracts coreContractAddress) (err ERR_CORE_CONTRACT_NOT_FOUND))) + ) + (asserts! (is-authorized-city) (err ERR_UNAUTHORIZED)) + (asserts! (is-eq coreContractAddress (var-get activeCoreContract)) (err ERR_UNAUTHORIZED)) + (var-set cityWallet newCityWallet) + (try! (contract-call? targetContract set-city-wallet newCityWallet)) + (ok true) + ) +) + +(define-public (execute-set-city-wallet-job (jobId uint) (targetContract )) + (let + ( + (coreContractAddress (contract-of targetContract)) + (coreContract (unwrap! (map-get? CoreContracts coreContractAddress) (err ERR_CORE_CONTRACT_NOT_FOUND))) + (newCityWallet (unwrap! (get-principal-value-by-name jobId "newCityWallet") (err ERR_UNKNOWN_ARGUMENT))) + ) + (asserts! (is-approver contract-caller) (err ERR_UNAUTHORIZED)) + (asserts! (is-eq coreContractAddress (var-get activeCoreContract)) (err ERR_UNAUTHORIZED)) + (var-set cityWallet newCityWallet) + (try! (contract-call? targetContract set-city-wallet newCityWallet)) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; check if contract caller is city wallet +(define-private (is-authorized-city) + (is-eq contract-caller (var-get cityWallet)) +) + +;; TOKEN MANAGEMENT + +(define-public (set-token-uri (targetContract ) (newUri (optional (string-utf8 256)))) + (begin + (asserts! (is-authorized-city) (err ERR_UNAUTHORIZED)) + (as-contract (try! (contract-call? targetContract set-token-uri newUri))) + (ok true) + ) +) + +;; APPROVERS MANAGEMENT + +(define-public (execute-replace-approver-job (jobId uint)) + (let + ( + (oldApprover (unwrap! (get-principal-value-by-name jobId "oldApprover") (err ERR_UNKNOWN_ARGUMENT))) + (newApprover (unwrap! (get-principal-value-by-name jobId "newApprover") (err ERR_UNKNOWN_ARGUMENT))) + ) + (asserts! (is-approver contract-caller) (err ERR_UNAUTHORIZED)) + (map-set Approvers oldApprover false) + (map-set Approvers newApprover true) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; CONTRACT INITIALIZATION + +(map-insert Approvers 'SP372JVX6EWE2M0XPA84MWZYRRG2M6CAC4VVC12V1 true) +(map-insert Approvers 'SP2R0DQYR7XHD161SH2GK49QRP1YSV7HE9JSG7W6G true) +(map-insert Approvers 'SPN4Y5QPGQA8882ZXW90ADC2DHYXMSTN8VAR8C3X true) +(map-insert Approvers 'SP3YYGCGX1B62CYAH4QX7PQE63YXG7RDTXD8BQHJQ true) +(map-insert Approvers 'SP7DGES13508FHRWS1FB0J3SZA326FP6QRMB6JDE true) diff --git a/.cache/requirements/SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-auth.json b/.cache/requirements/SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-auth.json new file mode 100644 index 0000000..11162a1 --- /dev/null +++ b/.cache/requirements/SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-auth.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch20", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token.clar b/.cache/requirements/SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token.clar new file mode 100644 index 0000000..0a04a10 --- /dev/null +++ b/.cache/requirements/SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token.clar @@ -0,0 +1,177 @@ +;; NEWYORKCITYCOIN TOKEN CONTRACT +;; CityCoins Protocol Version 1.0.1 + +;; CONTRACT OWNER + +(define-constant CONTRACT_OWNER tx-sender) + +;; TRAIT DEFINITIONS + +(impl-trait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-token-trait.citycoin-token) +(use-trait coreTrait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.citycoin-core) + +;; ERROR CODES + +(define-constant ERR_UNAUTHORIZED u2000) +(define-constant ERR_TOKEN_NOT_ACTIVATED u2001) +(define-constant ERR_TOKEN_ALREADY_ACTIVATED u2002) + +;; SIP-010 DEFINITION + +(impl-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait) + +(define-fungible-token newyorkcitycoin) + +;; SIP-010 FUNCTIONS + +(define-public (transfer (amount uint) (from principal) (to principal) (memo (optional (buff 34)))) + (begin + (asserts! (is-eq from tx-sender) (err ERR_UNAUTHORIZED)) + (if (is-some memo) + (print memo) + none + ) + (ft-transfer? newyorkcitycoin amount from to) + ) +) + +(define-read-only (get-name) + (ok "newyorkcitycoin") +) + +(define-read-only (get-symbol) + (ok "NYC") +) + +(define-read-only (get-decimals) + (ok u0) +) + +(define-read-only (get-balance (user principal)) + (ok (ft-get-balance newyorkcitycoin user)) +) + +(define-read-only (get-total-supply) + (ok (ft-get-supply newyorkcitycoin)) +) + +(define-read-only (get-token-uri) + (ok (var-get tokenUri)) +) + +;; TOKEN CONFIGURATION + +;; how many blocks until the next halving occurs +(define-constant TOKEN_HALVING_BLOCKS u210000) + +;; store block height at each halving, set by register-user in core contract +(define-data-var coinbaseThreshold1 uint u0) +(define-data-var coinbaseThreshold2 uint u0) +(define-data-var coinbaseThreshold3 uint u0) +(define-data-var coinbaseThreshold4 uint u0) +(define-data-var coinbaseThreshold5 uint u0) + +;; once activated, thresholds cannot be updated again +(define-data-var tokenActivated bool false) + +;; core contract states +(define-constant STATE_DEPLOYED u0) +(define-constant STATE_ACTIVE u1) +(define-constant STATE_INACTIVE u2) + +;; one-time function to activate the token +(define-public (activate-token (coreContract principal) (stacksHeight uint)) + (let + ( + (coreContractMap (try! (contract-call? 'SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-auth get-core-contract-info coreContract))) + ) + (asserts! (is-eq (get state coreContractMap) STATE_ACTIVE) (err ERR_UNAUTHORIZED)) + (asserts! (not (var-get tokenActivated)) (err ERR_TOKEN_ALREADY_ACTIVATED)) + (var-set tokenActivated true) + (var-set coinbaseThreshold1 (+ stacksHeight TOKEN_HALVING_BLOCKS)) + (var-set coinbaseThreshold2 (+ stacksHeight (* u2 TOKEN_HALVING_BLOCKS))) + (var-set coinbaseThreshold3 (+ stacksHeight (* u3 TOKEN_HALVING_BLOCKS))) + (var-set coinbaseThreshold4 (+ stacksHeight (* u4 TOKEN_HALVING_BLOCKS))) + (var-set coinbaseThreshold5 (+ stacksHeight (* u5 TOKEN_HALVING_BLOCKS))) + (ok true) + ) +) + +;; return coinbase thresholds if token activated +(define-read-only (get-coinbase-thresholds) + (let + ( + (activated (var-get tokenActivated)) + ) + (asserts! activated (err ERR_TOKEN_NOT_ACTIVATED)) + (ok { + coinbaseThreshold1: (var-get coinbaseThreshold1), + coinbaseThreshold2: (var-get coinbaseThreshold2), + coinbaseThreshold3: (var-get coinbaseThreshold3), + coinbaseThreshold4: (var-get coinbaseThreshold4), + coinbaseThreshold5: (var-get coinbaseThreshold5) + }) + ) +) + +;; UTILITIES + +(define-data-var tokenUri (optional (string-utf8 256)) (some u"https://cdn.citycoins.co/metadata/newyorkcitycoin.json")) + +;; set token URI to new value, only accessible by Auth +(define-public (set-token-uri (newUri (optional (string-utf8 256)))) + (begin + (asserts! (is-authorized-auth) (err ERR_UNAUTHORIZED)) + (ok (var-set tokenUri newUri)) + ) +) + +;; mint new tokens, only accessible by a Core contract +(define-public (mint (amount uint) (recipient principal)) + (let + ( + (coreContract (try! (contract-call? 'SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-auth get-core-contract-info contract-caller))) + ) + (ft-mint? newyorkcitycoin amount recipient) + ) +) + +(define-public (burn (amount uint) (owner principal)) + (begin + (asserts! (is-eq tx-sender owner) (err ERR_UNAUTHORIZED)) + (ft-burn? newyorkcitycoin amount owner) + ) +) + +;; checks if caller is Auth contract +(define-private (is-authorized-auth) + (is-eq contract-caller 'SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-auth) +) + +;; SEND-MANY + +(define-public (send-many (recipients (list 200 { to: principal, amount: uint, memo: (optional (buff 34)) }))) + (fold check-err + (map send-token recipients) + (ok true) + ) +) + +(define-private (check-err (result (response bool uint)) (prior (response bool uint))) + (match prior ok-value result + err-value (err err-value) + ) +) + +(define-private (send-token (recipient { to: principal, amount: uint, memo: (optional (buff 34)) })) + (send-token-with-memo (get amount recipient) (get to recipient) (get memo recipient)) +) + +(define-private (send-token-with-memo (amount uint) (to principal) (memo (optional (buff 34)))) + (let + ( + (transferOk (try! (transfer amount tx-sender to memo))) + ) + (ok transferOk) + ) +) diff --git a/.cache/requirements/SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token.json b/.cache/requirements/SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token.json new file mode 100644 index 0000000..11162a1 --- /dev/null +++ b/.cache/requirements/SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch20", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.clar b/.cache/requirements/SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.clar new file mode 100644 index 0000000..cc558fd --- /dev/null +++ b/.cache/requirements/SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.clar @@ -0,0 +1,15 @@ +(define-trait nft-trait + ( + ;; Last token ID, limited to uint range + (get-last-token-id () (response uint uint)) + + ;; URI for metadata associated with the token + (get-token-uri (uint) (response (optional (string-ascii 256)) uint)) + + ;; Owner of a given token identifier + (get-owner (uint) (response (optional principal) uint)) + + ;; Transfer from the sender to a new principal + (transfer (uint principal principal) (response bool uint)) + ) +) diff --git a/.cache/requirements/SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.json b/.cache/requirements/SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.json new file mode 100644 index 0000000..11162a1 --- /dev/null +++ b/.cache/requirements/SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch20", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.clar b/.cache/requirements/SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.clar new file mode 100644 index 0000000..69255cf --- /dev/null +++ b/.cache/requirements/SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.clar @@ -0,0 +1,24 @@ +(define-trait sip-010-trait + ( + ;; Transfer from the caller to a new principal + (transfer (uint principal principal (optional (buff 34))) (response bool uint)) + + ;; the human readable name of the token + (get-name () (response (string-ascii 32) uint)) + + ;; the ticker symbol, or empty if none + (get-symbol () (response (string-ascii 32) uint)) + + ;; the number of decimals used, e.g. 6 would mean 1_000_000 represents 1 token + (get-decimals () (response uint uint)) + + ;; the balance of the passed principal + (get-balance (principal) (response uint uint)) + + ;; the current total supply (which does not need to be a constant) + (get-total-supply () (response uint uint)) + + ;; an optional URI that represents metadata of this token + (get-token-uri () (response (optional (string-utf8 256)) uint)) + ) +) diff --git a/.cache/requirements/SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.json b/.cache/requirements/SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.json new file mode 100644 index 0000000..11162a1 --- /dev/null +++ b/.cache/requirements/SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch20", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.clar b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.clar new file mode 100644 index 0000000..b294532 --- /dev/null +++ b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.clar @@ -0,0 +1,35 @@ +;; CITYCOIN CORE TRAIT + +(define-trait citycoin-core + ( + + (register-user ((optional (string-utf8 50))) + (response bool uint) + ) + + (mine-tokens (uint (optional (buff 34))) + (response bool uint) + ) + + (claim-mining-reward (uint) + (response bool uint) + ) + + (stack-tokens (uint uint) + (response bool uint) + ) + + (claim-stacking-reward (uint) + (response bool uint) + ) + + (set-city-wallet (principal) + (response bool uint) + ) + + (shutdown-contract (uint) + (response bool uint) + ) + + ) +) diff --git a/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.json b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.json new file mode 100644 index 0000000..11162a1 --- /dev/null +++ b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch20", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-token-trait.clar b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-token-trait.clar new file mode 100644 index 0000000..107b4e4 --- /dev/null +++ b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-token-trait.clar @@ -0,0 +1,27 @@ +;; CITYCOIN TOKEN TRAIT + +(define-trait citycoin-token + ( + + (activate-token (principal uint) + (response bool uint) + ) + + (set-token-uri ((optional (string-utf8 256))) + (response bool uint) + ) + + (mint (uint principal) + (response bool uint) + ) + + (burn (uint principal) + (response bool uint) + ) + + (send-many ((list 200 { to: principal, amount: uint, memo: (optional (buff 34)) })) + (response bool uint) + ) + + ) +) diff --git a/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-token-trait.json b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-token-trait.json new file mode 100644 index 0000000..11162a1 --- /dev/null +++ b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-token-trait.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch20", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-auth.clar b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-auth.clar new file mode 100644 index 0000000..9f26454 --- /dev/null +++ b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-auth.clar @@ -0,0 +1,563 @@ +;; MIAMICOIN AUTH CONTRACT + +(define-constant CONTRACT_OWNER tx-sender) + +;; TRAIT DEFINITIONS + +(use-trait coreTrait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.citycoin-core) +(use-trait tokenTrait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-token-trait.citycoin-token) + +;; ERRORS + +(define-constant ERR_UNKNOWN_JOB u6000) +(define-constant ERR_UNAUTHORIZED u6001) +(define-constant ERR_JOB_IS_ACTIVE u6002) +(define-constant ERR_JOB_IS_NOT_ACTIVE u6003) +(define-constant ERR_ALREADY_VOTED_THIS_WAY u6004) +(define-constant ERR_JOB_IS_EXECUTED u6005) +(define-constant ERR_JOB_IS_NOT_APPROVED u6006) +(define-constant ERR_ARGUMENT_ALREADY_EXISTS u6007) +(define-constant ERR_NO_ACTIVE_CORE_CONTRACT u6008) +(define-constant ERR_CORE_CONTRACT_NOT_FOUND u6009) +(define-constant ERR_UNKNOWN_ARGUMENT u6010) + +;; JOB MANAGEMENT + +(define-constant REQUIRED_APPROVALS u3) + +(define-data-var lastJobId uint u0) + +(define-map Jobs + uint + { + creator: principal, + name: (string-ascii 255), + target: principal, + approvals: uint, + disapprovals: uint, + isActive: bool, + isExecuted: bool + } +) + +(define-map JobApprovers + { jobId: uint, approver: principal } + bool +) + +(define-map Approvers + principal + bool +) + +(define-map ArgumentLastIdsByType + { jobId: uint, argumentType: (string-ascii 25) } + uint +) + +(define-map UIntArgumentsByName + { jobId: uint, argumentName: (string-ascii 255) } + { argumentId: uint, value: uint} +) + +(define-map UIntArgumentsById + { jobId: uint, argumentId: uint } + { argumentName: (string-ascii 255), value: uint } +) + +(define-map PrincipalArgumentsByName + { jobId: uint, argumentName: (string-ascii 255) } + { argumentId: uint, value: principal } +) + +(define-map PrincipalArgumentsById + { jobId: uint, argumentId: uint } + { argumentName: (string-ascii 255), value: principal } +) + +;; FUNCTIONS + +(define-read-only (get-last-job-id) + (var-get lastJobId) +) + +(define-public (create-job (name (string-ascii 255)) (target principal)) + (let + ( + (newJobId (+ (var-get lastJobId) u1)) + ) + (asserts! (is-approver tx-sender) (err ERR_UNAUTHORIZED)) + (map-set Jobs + newJobId + { + creator: tx-sender, + name: name, + target: target, + approvals: u0, + disapprovals: u0, + isActive: false, + isExecuted: false + } + ) + (var-set lastJobId newJobId) + (ok newJobId) + ) +) + +(define-read-only (get-job (jobId uint)) + (map-get? Jobs jobId) +) + +(define-public (activate-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) (err ERR_UNKNOWN_JOB))) + ) + (asserts! (is-eq (get creator job) tx-sender) (err ERR_UNAUTHORIZED)) + (asserts! (not (get isActive job)) (err ERR_JOB_IS_ACTIVE)) + (map-set Jobs + jobId + (merge job { isActive: true }) + ) + (ok true) + ) +) + +(define-public (approve-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) (err ERR_UNKNOWN_JOB))) + (previousVote (map-get? JobApprovers { jobId: jobId, approver: tx-sender })) + ) + (asserts! (get isActive job) (err ERR_JOB_IS_NOT_ACTIVE)) + (asserts! (is-approver tx-sender) (err ERR_UNAUTHORIZED)) + ;; save vote + (map-set JobApprovers + { jobId: jobId, approver: tx-sender } + true + ) + (match previousVote approved + (begin + (asserts! (not approved) (err ERR_ALREADY_VOTED_THIS_WAY)) + (map-set Jobs jobId + (merge job + { + approvals: (+ (get approvals job) u1), + disapprovals: (- (get disapprovals job) u1) + } + ) + ) + ) + ;; no previous vote + (map-set Jobs + jobId + (merge job { approvals: (+ (get approvals job) u1) } ) + ) + ) + (ok true) + ) +) + +(define-public (disapprove-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) (err ERR_UNKNOWN_JOB))) + (previousVote (map-get? JobApprovers { jobId: jobId, approver: tx-sender })) + ) + (asserts! (get isActive job) (err ERR_JOB_IS_NOT_ACTIVE)) + (asserts! (is-approver tx-sender) (err ERR_UNAUTHORIZED)) + ;; save vote + (map-set JobApprovers + { jobId: jobId, approver: tx-sender } + false + ) + (match previousVote approved + (begin + (asserts! approved (err ERR_ALREADY_VOTED_THIS_WAY)) + (map-set Jobs jobId + (merge job + { + approvals: (- (get approvals job) u1), + disapprovals: (+ (get disapprovals job) u1) + } + ) + ) + ) + ;; no previous vote + (map-set Jobs + jobId + (merge job { disapprovals: (+ (get disapprovals job) u1) } ) + ) + ) + (ok true) + ) +) + +(define-read-only (is-job-approved (jobId uint)) + (match (get-job jobId) job + (>= (get approvals job) REQUIRED_APPROVALS) + false + ) +) + +(define-public (mark-job-as-executed (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) (err ERR_UNKNOWN_JOB))) + ) + (asserts! (get isActive job) (err ERR_JOB_IS_NOT_ACTIVE)) + (asserts! (>= (get approvals job) REQUIRED_APPROVALS) (err ERR_JOB_IS_NOT_APPROVED)) + (asserts! (is-eq (get target job) contract-caller) (err ERR_UNAUTHORIZED)) + (asserts! (not (get isExecuted job)) (err ERR_JOB_IS_EXECUTED)) + (map-set Jobs + jobId + (merge job { isExecuted: true }) + ) + (ok true) + ) +) + +(define-public (add-uint-argument (jobId uint) (argumentName (string-ascii 255)) (value uint)) + (let + ( + (argumentId (generate-argument-id jobId "uint")) + ) + (try! (guard-add-argument jobId)) + (asserts! + (and + (map-insert UIntArgumentsById + { jobId: jobId, argumentId: argumentId } + { argumentName: argumentName, value: value } + ) + (map-insert UIntArgumentsByName + { jobId: jobId, argumentName: argumentName } + { argumentId: argumentId, value: value} + ) + ) + (err ERR_ARGUMENT_ALREADY_EXISTS) + ) + (ok true) + ) +) + +(define-read-only (get-uint-argument-by-name (jobId uint) (argumentName (string-ascii 255))) + (map-get? UIntArgumentsByName { jobId: jobId, argumentName: argumentName }) +) + +(define-read-only (get-uint-argument-by-id (jobId uint) (argumentId uint)) + (map-get? UIntArgumentsById { jobId: jobId, argumentId: argumentId }) +) + +(define-read-only (get-uint-value-by-name (jobId uint) (argumentName (string-ascii 255))) + (get value (get-uint-argument-by-name jobId argumentName)) +) + +(define-read-only (get-uint-value-by-id (jobId uint) (argumentId uint)) + (get value (get-uint-argument-by-id jobId argumentId)) +) + +(define-public (add-principal-argument (jobId uint) (argumentName (string-ascii 255)) (value principal)) + (let + ( + (argumentId (generate-argument-id jobId "principal")) + ) + (try! (guard-add-argument jobId)) + (asserts! + (and + (map-insert PrincipalArgumentsById + { jobId: jobId, argumentId: argumentId } + { argumentName: argumentName, value: value } + ) + (map-insert PrincipalArgumentsByName + { jobId: jobId, argumentName: argumentName } + { argumentId: argumentId, value: value} + ) + ) + (err ERR_ARGUMENT_ALREADY_EXISTS) + ) + (ok true) + ) +) + +(define-read-only (get-principal-argument-by-name (jobId uint) (argumentName (string-ascii 255))) + (map-get? PrincipalArgumentsByName { jobId: jobId, argumentName: argumentName }) +) + +(define-read-only (get-principal-argument-by-id (jobId uint) (argumentId uint)) + (map-get? PrincipalArgumentsById { jobId: jobId, argumentId: argumentId }) +) + +(define-read-only (get-principal-value-by-name (jobId uint) (argumentName (string-ascii 255))) + (get value (get-principal-argument-by-name jobId argumentName)) +) + +(define-read-only (get-principal-value-by-id (jobId uint) (argumentId uint)) + (get value (get-principal-argument-by-id jobId argumentId)) +) + +;; PRIVATE FUNCTIONS + +(define-read-only (is-approver (user principal)) + (default-to false (map-get? Approvers user)) +) + +(define-private (generate-argument-id (jobId uint) (argumentType (string-ascii 25))) + (let + ( + (argumentId (+ (default-to u0 (map-get? ArgumentLastIdsByType { jobId: jobId, argumentType: argumentType })) u1)) + ) + (map-set ArgumentLastIdsByType + { jobId: jobId, argumentType: argumentType } + argumentId + ) + ;; return + argumentId + ) +) + +(define-private (guard-add-argument (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) (err ERR_UNKNOWN_JOB))) + ) + (asserts! (not (get isActive job)) (err ERR_JOB_IS_ACTIVE)) + (asserts! (is-eq (get creator job) contract-caller) (err ERR_UNAUTHORIZED)) + (ok true) + ) +) + +;; CONTRACT MANAGEMENT + +;; initial value for active core contract +;; set to deployer address at startup to prevent +;; circular dependency of core on auth +(define-data-var activeCoreContract principal CONTRACT_OWNER) +(define-data-var initialized bool false) + +;; core contract states +(define-constant STATE_DEPLOYED u0) +(define-constant STATE_ACTIVE u1) +(define-constant STATE_INACTIVE u2) + +;; core contract map +(define-map CoreContracts + principal + { + state: uint, + startHeight: uint, + endHeight: uint + } +) + +;; getter for active core contract +(define-read-only (get-active-core-contract) + (begin + (asserts! (not (is-eq (var-get activeCoreContract) CONTRACT_OWNER)) (err ERR_NO_ACTIVE_CORE_CONTRACT)) + (ok (var-get activeCoreContract)) + ) +) + +;; getter for core contract map +(define-read-only (get-core-contract-info (targetContract principal)) + (let + ( + (coreContract (unwrap! (map-get? CoreContracts targetContract) (err ERR_CORE_CONTRACT_NOT_FOUND))) + ) + (ok coreContract) + ) +) + +;; one-time function to initialize contracts after all contracts are deployed +;; - check that deployer is calling this function +;; - check this contract is not activated already (one-time use) +;; - set initial map value for core contract v1 +;; - set cityWallet in core contract +;; - set intialized true +(define-public (initialize-contracts (coreContract )) + (let + ( + (coreContractAddress (contract-of coreContract)) + ) + (asserts! (is-eq contract-caller CONTRACT_OWNER) (err ERR_UNAUTHORIZED)) + (asserts! (not (var-get initialized)) (err ERR_UNAUTHORIZED)) + (map-set CoreContracts + coreContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (try! (contract-call? coreContract set-city-wallet (var-get cityWallet))) + (var-set initialized true) + (ok true) + ) +) + +(define-read-only (is-initialized) + (var-get initialized) +) + +;; function to activate core contract through registration +;; - check that target is in core contract map +;; - check that caller is core contract +;; - set active in core contract map +;; - set as activeCoreContract +(define-public (activate-core-contract (targetContract principal) (stacksHeight uint)) + (let + ( + (coreContract (unwrap! (map-get? CoreContracts targetContract) (err ERR_CORE_CONTRACT_NOT_FOUND))) + ) + (asserts! (is-eq contract-caller targetContract) (err ERR_UNAUTHORIZED)) + (map-set CoreContracts + targetContract + { + state: STATE_ACTIVE, + startHeight: stacksHeight, + endHeight: u0 + }) + (var-set activeCoreContract targetContract) + (ok true) + ) +) + +;; protected function to update core contract +(define-public (upgrade-core-contract (oldContract ) (newContract )) + (let + ( + (oldContractAddress (contract-of oldContract)) + (oldContractMap (unwrap! (map-get? CoreContracts oldContractAddress) (err ERR_CORE_CONTRACT_NOT_FOUND))) + (newContractAddress (contract-of newContract)) + ) + (asserts! (not (is-eq oldContractAddress newContractAddress)) (err ERR_UNAUTHORIZED)) + (asserts! (is-authorized-city) (err ERR_UNAUTHORIZED)) + (map-set CoreContracts + oldContractAddress + { + state: STATE_INACTIVE, + startHeight: (get startHeight oldContractMap), + endHeight: block-height + }) + (map-set CoreContracts + newContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (var-set activeCoreContract newContractAddress) + (try! (contract-call? oldContract shutdown-contract block-height)) + (try! (contract-call? newContract set-city-wallet (var-get cityWallet))) + (ok true) + ) +) + +(define-public (execute-upgrade-core-contract-job (jobId uint) (oldContract ) (newContract )) + (let + ( + (oldContractArg (unwrap! (get-principal-value-by-name jobId "oldContract") (err ERR_UNKNOWN_ARGUMENT))) + (newContractArg (unwrap! (get-principal-value-by-name jobId "newContract") (err ERR_UNKNOWN_ARGUMENT))) + (oldContractAddress (contract-of oldContract)) + (oldContractMap (unwrap! (map-get? CoreContracts oldContractAddress) (err ERR_CORE_CONTRACT_NOT_FOUND))) + (newContractAddress (contract-of newContract)) + ) + (asserts! (is-approver contract-caller) (err ERR_UNAUTHORIZED)) + (asserts! (and (is-eq oldContractArg oldContractAddress) (is-eq newContractArg newContractAddress)) (err ERR_UNAUTHORIZED)) + (asserts! (not (is-eq oldContractAddress newContractAddress)) (err ERR_UNAUTHORIZED)) + (map-set CoreContracts + oldContractAddress + { + state: STATE_INACTIVE, + startHeight: (get startHeight oldContractMap), + endHeight: block-height + }) + (map-set CoreContracts + newContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (var-set activeCoreContract newContractAddress) + (try! (contract-call? oldContract shutdown-contract block-height)) + (try! (contract-call? newContract set-city-wallet (var-get cityWallet))) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; CITY WALLET MANAGEMENT + +;; initial value for city wallet +(define-data-var cityWallet principal 'SM2MARAVW6BEJCD13YV2RHGYHQWT7TDDNMNRB1MVT) + +;; returns city wallet principal +(define-read-only (get-city-wallet) + (ok (var-get cityWallet)) +) + +;; protected function to update city wallet variable +(define-public (set-city-wallet (targetContract ) (newCityWallet principal)) + (let + ( + (coreContractAddress (contract-of targetContract)) + (coreContract (unwrap! (map-get? CoreContracts coreContractAddress) (err ERR_CORE_CONTRACT_NOT_FOUND))) + ) + (asserts! (is-authorized-city) (err ERR_UNAUTHORIZED)) + (asserts! (is-eq coreContractAddress (var-get activeCoreContract)) (err ERR_UNAUTHORIZED)) + (var-set cityWallet newCityWallet) + (try! (contract-call? targetContract set-city-wallet newCityWallet)) + (ok true) + ) +) + +(define-public (execute-set-city-wallet-job (jobId uint) (targetContract )) + (let + ( + (coreContractAddress (contract-of targetContract)) + (coreContract (unwrap! (map-get? CoreContracts coreContractAddress) (err ERR_CORE_CONTRACT_NOT_FOUND))) + (newCityWallet (unwrap! (get-principal-value-by-name jobId "newCityWallet") (err ERR_UNKNOWN_ARGUMENT))) + ) + (asserts! (is-approver contract-caller) (err ERR_UNAUTHORIZED)) + (asserts! (is-eq coreContractAddress (var-get activeCoreContract)) (err ERR_UNAUTHORIZED)) + (var-set cityWallet newCityWallet) + (try! (contract-call? targetContract set-city-wallet newCityWallet)) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; check if contract caller is city wallet +(define-private (is-authorized-city) + (is-eq contract-caller (var-get cityWallet)) +) + +;; TOKEN MANAGEMENT + +(define-public (set-token-uri (targetContract ) (newUri (optional (string-utf8 256)))) + (begin + (asserts! (is-authorized-city) (err ERR_UNAUTHORIZED)) + (as-contract (try! (contract-call? targetContract set-token-uri newUri))) + (ok true) + ) +) + +;; APPROVERS MANAGEMENT + +(define-public (execute-replace-approver-job (jobId uint)) + (let + ( + (oldApprover (unwrap! (get-principal-value-by-name jobId "oldApprover") (err ERR_UNKNOWN_ARGUMENT))) + (newApprover (unwrap! (get-principal-value-by-name jobId "newApprover") (err ERR_UNKNOWN_ARGUMENT))) + ) + (asserts! (is-approver contract-caller) (err ERR_UNAUTHORIZED)) + (map-set Approvers oldApprover false) + (map-set Approvers newApprover true) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; CONTRACT INITIALIZATION + +(map-insert Approvers 'SP372JVX6EWE2M0XPA84MWZYRRG2M6CAC4VVC12V1 true) +(map-insert Approvers 'SP2R0DQYR7XHD161SH2GK49QRP1YSV7HE9JSG7W6G true) +(map-insert Approvers 'SPN4Y5QPGQA8882ZXW90ADC2DHYXMSTN8VAR8C3X true) +(map-insert Approvers 'SP3YYGCGX1B62CYAH4QX7PQE63YXG7RDTXD8BQHJQ true) +(map-insert Approvers 'SP7DGES13508FHRWS1FB0J3SZA326FP6QRMB6JDE true) diff --git a/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-auth.json b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-auth.json new file mode 100644 index 0000000..11162a1 --- /dev/null +++ b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-auth.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch20", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-token.clar b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-token.clar new file mode 100644 index 0000000..5631b64 --- /dev/null +++ b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-token.clar @@ -0,0 +1,179 @@ +;; MIAMICOIN TOKEN CONTRACT + +;; CONTRACT OWNER + +(define-constant CONTRACT_OWNER tx-sender) + +;; TRAIT DEFINITIONS + +(impl-trait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-token-trait.citycoin-token) +(use-trait coreTrait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.citycoin-core) + +;; ERROR CODES + +(define-constant ERR_UNAUTHORIZED u2000) +(define-constant ERR_TOKEN_NOT_ACTIVATED u2001) +(define-constant ERR_TOKEN_ALREADY_ACTIVATED u2002) + +;; SIP-010 DEFINITION + +(impl-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait) + +(define-fungible-token miamicoin) + +;; SIP-010 FUNCTIONS + +(define-public (transfer (amount uint) (from principal) (to principal) (memo (optional (buff 34)))) + (begin + (asserts! (is-eq from tx-sender) (err ERR_UNAUTHORIZED)) + (if (is-some memo) + (print memo) + none + ) + (ft-transfer? miamicoin amount from to) + ) +) + +(define-read-only (get-name) + (ok "miamicoin") +) + +(define-read-only (get-symbol) + (ok "MIA") +) + +(define-read-only (get-decimals) + (ok u0) +) + +(define-read-only (get-balance (user principal)) + (ok (ft-get-balance miamicoin user)) +) + +(define-read-only (get-total-supply) + (ok (ft-get-supply miamicoin)) +) + +(define-read-only (get-token-uri) + (ok (var-get tokenUri)) +) + +;; TOKEN CONFIGURATION + +;; how many blocks until the next halving occurs +(define-constant TOKEN_HALVING_BLOCKS u210000) + +;; store block height at each halving, set by register-user in core contract +(define-data-var coinbaseThreshold1 uint u0) +(define-data-var coinbaseThreshold2 uint u0) +(define-data-var coinbaseThreshold3 uint u0) +(define-data-var coinbaseThreshold4 uint u0) +(define-data-var coinbaseThreshold5 uint u0) + +;; once activated, thresholds cannot be updated again +(define-data-var tokenActivated bool false) + +;; core contract states +(define-constant STATE_DEPLOYED u0) +(define-constant STATE_ACTIVE u1) +(define-constant STATE_INACTIVE u2) + +;; one-time function to activate the token +(define-public (activate-token (coreContract principal) (stacksHeight uint)) + (let + ( + (coreContractMap (try! (contract-call? 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-auth get-core-contract-info coreContract))) + ) + (asserts! (is-eq (get state coreContractMap) STATE_ACTIVE) (err ERR_UNAUTHORIZED)) + (asserts! (not (var-get tokenActivated)) (err ERR_TOKEN_ALREADY_ACTIVATED)) + (var-set tokenActivated true) + (var-set coinbaseThreshold1 (+ stacksHeight TOKEN_HALVING_BLOCKS)) + (var-set coinbaseThreshold2 (+ stacksHeight (* u2 TOKEN_HALVING_BLOCKS))) + (var-set coinbaseThreshold3 (+ stacksHeight (* u3 TOKEN_HALVING_BLOCKS))) + (var-set coinbaseThreshold4 (+ stacksHeight (* u4 TOKEN_HALVING_BLOCKS))) + (var-set coinbaseThreshold5 (+ stacksHeight (* u5 TOKEN_HALVING_BLOCKS))) + (ok true) + ) +) + +;; return coinbase thresholds if token activated +(define-read-only (get-coinbase-thresholds) + (let + ( + (activated (var-get tokenActivated)) + ) + (asserts! activated (err ERR_TOKEN_NOT_ACTIVATED)) + (ok { + coinbaseThreshold1: (var-get coinbaseThreshold1), + coinbaseThreshold2: (var-get coinbaseThreshold2), + coinbaseThreshold3: (var-get coinbaseThreshold3), + coinbaseThreshold4: (var-get coinbaseThreshold4), + coinbaseThreshold5: (var-get coinbaseThreshold5) + }) + ) +) + +;; UTILITIES + +(define-data-var tokenUri (optional (string-utf8 256)) (some u"https://cdn.citycoins.co/metadata/miamicoin.json")) + +;; set token URI to new value, only accessible by Auth +(define-public (set-token-uri (newUri (optional (string-utf8 256)))) + (begin + (asserts! (is-authorized-auth) (err ERR_UNAUTHORIZED)) + (ok (var-set tokenUri newUri)) + ) +) + +;; mint new tokens, only accessible by a Core contract +(define-public (mint (amount uint) (recipient principal)) + (let + ( + (coreContract (try! (contract-call? 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-auth get-core-contract-info contract-caller))) + ) + (ft-mint? miamicoin amount recipient) + ) +) + +;; burn tokens, only accessible by a Core contract +(define-public (burn (amount uint) (recipient principal)) + (let + ( + (coreContract (try! (contract-call? 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-auth get-core-contract-info contract-caller))) + ) + (ft-burn? miamicoin amount recipient) + ) +) + +;; checks if caller is Auth contract +(define-private (is-authorized-auth) + (is-eq contract-caller 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-auth) +) + +;; SEND-MANY + +(define-public (send-many (recipients (list 200 { to: principal, amount: uint, memo: (optional (buff 34)) }))) + (fold check-err + (map send-token recipients) + (ok true) + ) +) + +(define-private (check-err (result (response bool uint)) (prior (response bool uint))) + (match prior ok-value result + err-value (err err-value) + ) +) + +(define-private (send-token (recipient { to: principal, amount: uint, memo: (optional (buff 34)) })) + (send-token-with-memo (get amount recipient) (get to recipient) (get memo recipient)) +) + +(define-private (send-token-with-memo (amount uint) (to principal) (memo (optional (buff 34)))) + (let + ( + (transferOk (try! (transfer amount tx-sender to memo))) + ) + (ok transferOk) + ) +) diff --git a/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-token.json b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-token.json new file mode 100644 index 0000000..11162a1 --- /dev/null +++ b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-token.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch20", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-core-v2-trait.clar b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-core-v2-trait.clar new file mode 100644 index 0000000..ebb6539 --- /dev/null +++ b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-core-v2-trait.clar @@ -0,0 +1,47 @@ +;; CITYCOIN CORE TRAIT V2 + +(define-trait citycoin-core-v2 + ( + + (register-user ((optional (string-utf8 50))) + (response bool uint) + ) + + (mine-tokens (uint (optional (buff 34))) + (response bool uint) + ) + + (mine-many ((list 200 uint)) + (response bool uint) + ) + + (claim-mining-reward (uint) + (response bool uint) + ) + + (stack-tokens (uint uint) + (response bool uint) + ) + + (claim-stacking-reward (uint) + (response bool uint) + ) + + (set-city-wallet (principal) + (response bool uint) + ) + + (update-coinbase-amounts () + (response bool uint) + ) + + (update-coinbase-thresholds () + (response bool uint) + ) + + (shutdown-contract (uint) + (response bool uint) + ) + + ) +) diff --git a/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-core-v2-trait.json b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-core-v2-trait.json new file mode 100644 index 0000000..89c8a70 --- /dev/null +++ b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-core-v2-trait.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-token-v2-trait.clar b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-token-v2-trait.clar new file mode 100644 index 0000000..794ebda --- /dev/null +++ b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-token-v2-trait.clar @@ -0,0 +1,35 @@ +;; CITYCOIN TOKEN TRAIT + +(define-trait citycoin-token-v2 + ( + + (activate-token (principal uint) + (response bool uint) + ) + + (set-token-uri ((optional (string-utf8 256))) + (response bool uint) + ) + + (mint (uint principal) + (response bool uint) + ) + + (burn (uint principal) + (response bool uint) + ) + + (send-many ((list 200 { to: principal, amount: uint, memo: (optional (buff 34)) })) + (response bool uint) + ) + + (update-coinbase-thresholds (uint uint uint uint uint) + (response bool uint) + ) + + (update-coinbase-amounts (uint uint uint uint uint uint uint) + (response bool uint) + ) + + ) +) diff --git a/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-token-v2-trait.json b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-token-v2-trait.json new file mode 100644 index 0000000..89c8a70 --- /dev/null +++ b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-token-v2-trait.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-vrf-v2.clar b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-vrf-v2.clar new file mode 100644 index 0000000..1278c67 --- /dev/null +++ b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-vrf-v2.clar @@ -0,0 +1,69 @@ +;; CITYCOIN VRF CONTRACT V2 +;; CityCoins Protocol Version 2.0.0 + +;; ERROR CODES + +(define-constant ERR_FAIL (err u3000)) + +;; CONFIGURATION + +(define-map RandomUintAtBlock uint uint) + +;; PUBLIC FUNCTIONS + +;; returns the saved random integer +;; or, calculates and saves it to the map +;; #[allow(unchecked_data)] +(define-public (get-save-rnd (block uint)) + (match (map-get? RandomUintAtBlock block) + rnd (ok rnd) + (match (read-rnd block) + rnd (begin (map-set RandomUintAtBlock block rnd) (ok rnd)) + err-val (err err-val) + ) + ) +) + +;; returns the saved random integer +;; or, calculates and returns the value +(define-read-only (get-rnd (block uint)) + (match (map-get? RandomUintAtBlock block) + rnd (ok rnd) + (read-rnd block) + ) +) + +;; PRIVATE FUNCTIONS + +(define-private (read-rnd (block uint)) + (ok (lower-16-le (unwrap! (get-block-info? vrf-seed block) ERR_FAIL))) +) + +(define-private (lower-16-le (vrfSeed (buff 32))) + (+ + (lower-16-le-inner (element-at vrfSeed u16) u15) + (lower-16-le-inner (element-at vrfSeed u17) u14) + (lower-16-le-inner (element-at vrfSeed u18) u13) + (lower-16-le-inner (element-at vrfSeed u19) u12) + (lower-16-le-inner (element-at vrfSeed u20) u11) + (lower-16-le-inner (element-at vrfSeed u21) u10) + (lower-16-le-inner (element-at vrfSeed u22) u9) + (lower-16-le-inner (element-at vrfSeed u23) u8) + (lower-16-le-inner (element-at vrfSeed u24) u7) + (lower-16-le-inner (element-at vrfSeed u25) u6) + (lower-16-le-inner (element-at vrfSeed u26) u5) + (lower-16-le-inner (element-at vrfSeed u27) u4) + (lower-16-le-inner (element-at vrfSeed u28) u3) + (lower-16-le-inner (element-at vrfSeed u29) u2) + (lower-16-le-inner (element-at vrfSeed u30) u1) + (lower-16-le-inner (element-at vrfSeed u31) u0) + ) +) + +(define-private (lower-16-le-inner (byte (optional (buff 1))) (pos uint)) + (* (buff-to-u8 (unwrap-panic byte)) (pow u2 (* u8 pos))) +) + +(define-private (buff-to-u8 (byte (buff 1))) + (unwrap-panic (index-of 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff byte)) +) diff --git a/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-vrf-v2.json b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-vrf-v2.json new file mode 100644 index 0000000..89c8a70 --- /dev/null +++ b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-vrf-v2.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-auth-v2.clar b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-auth-v2.clar new file mode 100644 index 0000000..7c7379d --- /dev/null +++ b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-auth-v2.clar @@ -0,0 +1,630 @@ +;; NEWYORKCITYCOIN AUTH CONTRACT V2 +;; CityCoins Protocol Version 2.0.0 + +(define-constant CONTRACT_OWNER tx-sender) + +;; TRAIT DEFINITIONS + +(use-trait coreTraitV2 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-core-v2-trait.citycoin-core-v2) +(use-trait tokenTraitV2 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-token-v2-trait.citycoin-token-v2) + +;; ERRORS + +(define-constant ERR_UNKNOWN_JOB (err u6000)) +(define-constant ERR_UNAUTHORIZED (err u6001)) +(define-constant ERR_JOB_IS_ACTIVE (err u6002)) +(define-constant ERR_JOB_IS_NOT_ACTIVE (err u6003)) +(define-constant ERR_ALREADY_VOTED_THIS_WAY (err u6004)) +(define-constant ERR_JOB_IS_EXECUTED (err u6005)) +(define-constant ERR_JOB_IS_NOT_APPROVED (err u6006)) +(define-constant ERR_ARGUMENT_ALREADY_EXISTS (err u6007)) +(define-constant ERR_NO_ACTIVE_CORE_CONTRACT (err u6008)) +(define-constant ERR_CORE_CONTRACT_NOT_FOUND (err u6009)) +(define-constant ERR_UNKNOWN_ARGUMENT (err u6010)) +(define-constant ERR_INCORRECT_CONTRACT_STATE (err u6011)) +(define-constant ERR_CONTRACT_ALREADY_EXISTS (err u6012)) + +;; JOB MANAGEMENT + +(define-constant REQUIRED_APPROVALS u3) + +(define-data-var lastJobId uint u0) + +(define-map Jobs + uint + { + creator: principal, + name: (string-ascii 255), + target: principal, + approvals: uint, + disapprovals: uint, + isActive: bool, + isExecuted: bool + } +) + +(define-map JobApprovers + { jobId: uint, approver: principal } + bool +) + +(define-map Approvers + principal + bool +) + +(define-map ArgumentLastIdsByType + { jobId: uint, argumentType: (string-ascii 25) } + uint +) + +(define-map UIntArgumentsByName + { jobId: uint, argumentName: (string-ascii 255) } + { argumentId: uint, value: uint} +) + +(define-map UIntArgumentsById + { jobId: uint, argumentId: uint } + { argumentName: (string-ascii 255), value: uint } +) + +(define-map PrincipalArgumentsByName + { jobId: uint, argumentName: (string-ascii 255) } + { argumentId: uint, value: principal } +) + +(define-map PrincipalArgumentsById + { jobId: uint, argumentId: uint } + { argumentName: (string-ascii 255), value: principal } +) + +;; FUNCTIONS + +(define-read-only (get-last-job-id) + (var-get lastJobId) +) + +(define-public (create-job (name (string-ascii 255)) (target principal)) + (let + ( + (newJobId (+ (var-get lastJobId) u1)) + ) + (asserts! (is-approver tx-sender) ERR_UNAUTHORIZED) + (map-set Jobs + newJobId + { + creator: tx-sender, + name: name, + target: target, + approvals: u0, + disapprovals: u0, + isActive: false, + isExecuted: false + } + ) + (var-set lastJobId newJobId) + (ok newJobId) + ) +) + +(define-read-only (get-job (jobId uint)) + (map-get? Jobs jobId) +) + +(define-public (activate-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + ) + (asserts! (is-eq (get creator job) tx-sender) ERR_UNAUTHORIZED) + (asserts! (not (get isActive job)) ERR_JOB_IS_ACTIVE) + (map-set Jobs + jobId + (merge job { isActive: true }) + ) + (ok true) + ) +) + +(define-public (approve-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + (previousVote (map-get? JobApprovers { jobId: jobId, approver: tx-sender })) + ) + (asserts! (get isActive job) ERR_JOB_IS_NOT_ACTIVE) + (asserts! (is-approver tx-sender) ERR_UNAUTHORIZED) + ;; save vote + (map-set JobApprovers + { jobId: jobId, approver: tx-sender } + true + ) + (match previousVote approved + (begin + (asserts! (not approved) ERR_ALREADY_VOTED_THIS_WAY) + (map-set Jobs jobId + (merge job + { + approvals: (+ (get approvals job) u1), + disapprovals: (- (get disapprovals job) u1) + } + ) + ) + ) + ;; no previous vote + (map-set Jobs + jobId + (merge job { approvals: (+ (get approvals job) u1) } ) + ) + ) + (ok true) + ) +) + +(define-public (disapprove-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + (previousVote (map-get? JobApprovers { jobId: jobId, approver: tx-sender })) + ) + (asserts! (get isActive job) ERR_JOB_IS_NOT_ACTIVE) + (asserts! (is-approver tx-sender) ERR_UNAUTHORIZED) + ;; save vote + (map-set JobApprovers + { jobId: jobId, approver: tx-sender } + false + ) + (match previousVote approved + (begin + (asserts! approved ERR_ALREADY_VOTED_THIS_WAY) + (map-set Jobs jobId + (merge job + { + approvals: (- (get approvals job) u1), + disapprovals: (+ (get disapprovals job) u1) + } + ) + ) + ) + ;; no previous vote + (map-set Jobs + jobId + (merge job { disapprovals: (+ (get disapprovals job) u1) } ) + ) + ) + (ok true) + ) +) + +(define-read-only (is-job-approved (jobId uint)) + (match (get-job jobId) job + (>= (get approvals job) REQUIRED_APPROVALS) + false + ) +) + +(define-public (mark-job-as-executed (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + ) + (asserts! (get isActive job) ERR_JOB_IS_NOT_ACTIVE) + (asserts! (>= (get approvals job) REQUIRED_APPROVALS) ERR_JOB_IS_NOT_APPROVED) + (asserts! (is-eq (get target job) contract-caller) ERR_UNAUTHORIZED) + (asserts! (not (get isExecuted job)) ERR_JOB_IS_EXECUTED) + (map-set Jobs + jobId + (merge job { isExecuted: true }) + ) + (ok true) + ) +) + +(define-public (add-uint-argument (jobId uint) (argumentName (string-ascii 255)) (value uint)) + (let + ( + (argumentId (generate-argument-id jobId "uint")) + ) + (try! (guard-add-argument jobId)) + (asserts! + (and + (map-insert UIntArgumentsById + { jobId: jobId, argumentId: argumentId } + { argumentName: argumentName, value: value } + ) + (map-insert UIntArgumentsByName + { jobId: jobId, argumentName: argumentName } + { argumentId: argumentId, value: value} + ) + ) + ERR_ARGUMENT_ALREADY_EXISTS + ) + (ok true) + ) +) + +(define-read-only (get-uint-argument-by-name (jobId uint) (argumentName (string-ascii 255))) + (map-get? UIntArgumentsByName { jobId: jobId, argumentName: argumentName }) +) + +(define-read-only (get-uint-argument-by-id (jobId uint) (argumentId uint)) + (map-get? UIntArgumentsById { jobId: jobId, argumentId: argumentId }) +) + +(define-read-only (get-uint-value-by-name (jobId uint) (argumentName (string-ascii 255))) + (get value (get-uint-argument-by-name jobId argumentName)) +) + +(define-read-only (get-uint-value-by-id (jobId uint) (argumentId uint)) + (get value (get-uint-argument-by-id jobId argumentId)) +) + +(define-public (add-principal-argument (jobId uint) (argumentName (string-ascii 255)) (value principal)) + (let + ( + (argumentId (generate-argument-id jobId "principal")) + ) + (try! (guard-add-argument jobId)) + (asserts! + (and + (map-insert PrincipalArgumentsById + { jobId: jobId, argumentId: argumentId } + { argumentName: argumentName, value: value } + ) + (map-insert PrincipalArgumentsByName + { jobId: jobId, argumentName: argumentName } + { argumentId: argumentId, value: value} + ) + ) + ERR_ARGUMENT_ALREADY_EXISTS + ) + (ok true) + ) +) + +(define-read-only (get-principal-argument-by-name (jobId uint) (argumentName (string-ascii 255))) + (map-get? PrincipalArgumentsByName { jobId: jobId, argumentName: argumentName }) +) + +(define-read-only (get-principal-argument-by-id (jobId uint) (argumentId uint)) + (map-get? PrincipalArgumentsById { jobId: jobId, argumentId: argumentId }) +) + +(define-read-only (get-principal-value-by-name (jobId uint) (argumentName (string-ascii 255))) + (get value (get-principal-argument-by-name jobId argumentName)) +) + +(define-read-only (get-principal-value-by-id (jobId uint) (argumentId uint)) + (get value (get-principal-argument-by-id jobId argumentId)) +) + +;; PRIVATE FUNCTIONS + +(define-read-only (is-approver (user principal)) + (default-to false (map-get? Approvers user)) +) + +(define-private (generate-argument-id (jobId uint) (argumentType (string-ascii 25))) + (let + ( + (argumentId (+ (default-to u0 (map-get? ArgumentLastIdsByType { jobId: jobId, argumentType: argumentType })) u1)) + ) + (map-set ArgumentLastIdsByType + { jobId: jobId, argumentType: argumentType } + argumentId + ) + ;; return + argumentId + ) +) + +(define-private (guard-add-argument (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + ) + (asserts! (not (get isActive job)) ERR_JOB_IS_ACTIVE) + (asserts! (is-eq (get creator job) contract-caller) ERR_UNAUTHORIZED) + (ok true) + ) +) + +;; CONTRACT MANAGEMENT + +;; initial value for active core contract +;; set to deployer address at startup to prevent +;; circular dependency of core on auth +(define-data-var activeCoreContract principal CONTRACT_OWNER) +(define-data-var initialized bool false) + +;; core contract states +(define-constant STATE_DEPLOYED u0) +(define-constant STATE_ACTIVE u1) +(define-constant STATE_INACTIVE u2) + +;; core contract map +(define-map CoreContracts + principal + { + state: uint, + startHeight: uint, + endHeight: uint + } +) + +;; getter for active core contract +(define-read-only (get-active-core-contract) + (begin + (asserts! (not (is-eq (var-get activeCoreContract) CONTRACT_OWNER)) ERR_NO_ACTIVE_CORE_CONTRACT) + (ok (var-get activeCoreContract)) + ) +) + +;; getter for core contract map +(define-read-only (get-core-contract-info (targetContract principal)) + (let + ( + (coreContract (unwrap! (map-get? CoreContracts targetContract) ERR_CORE_CONTRACT_NOT_FOUND)) + ) + (ok coreContract) + ) +) + +;; one-time function to initialize contracts after all contracts are deployed +;; - check that deployer is calling this function +;; - check this contract is not activated already (one-time use) +;; - set initial map value for core contract v1 +;; - set cityWallet in core contract +;; - set intialized true +(define-public (initialize-contracts (coreContract )) + (let + ( + (coreContractAddress (contract-of coreContract)) + ) + (asserts! (is-eq contract-caller CONTRACT_OWNER) ERR_UNAUTHORIZED) + (asserts! (not (var-get initialized)) ERR_UNAUTHORIZED) + (map-set CoreContracts + coreContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (try! (contract-call? coreContract set-city-wallet (var-get cityWallet))) + (var-set initialized true) + (ok true) + ) +) + +(define-read-only (is-initialized) + (var-get initialized) +) + +;; function to activate core contract through registration +;; - check that target is in core contract map +;; - check that caller is core contract +;; - check that target is in STATE_DEPLOYED +;; - set active in core contract map +;; - set as activeCoreContract +(define-public (activate-core-contract (targetContract principal) (stacksHeight uint)) + (let + ( + (coreContract (unwrap! (map-get? CoreContracts targetContract) ERR_CORE_CONTRACT_NOT_FOUND)) + ) + (asserts! (is-eq (get state coreContract) STATE_DEPLOYED) ERR_INCORRECT_CONTRACT_STATE) + (asserts! (is-eq contract-caller targetContract) ERR_UNAUTHORIZED) + (map-set CoreContracts + targetContract + { + state: STATE_ACTIVE, + startHeight: stacksHeight, + endHeight: u0 + }) + (var-set activeCoreContract targetContract) + (ok true) + ) +) + +;; protected function to update core contract +(define-public (upgrade-core-contract (oldContract ) (newContract )) + (let + ( + (oldContractAddress (contract-of oldContract)) + (oldContractMap (unwrap! (map-get? CoreContracts oldContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + (newContractAddress (contract-of newContract)) + ) + (asserts! (not (is-eq oldContractAddress newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (is-none (map-get? CoreContracts newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + (map-set CoreContracts + oldContractAddress + { + state: STATE_INACTIVE, + startHeight: (get startHeight oldContractMap), + endHeight: block-height + }) + (map-set CoreContracts + newContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (var-set activeCoreContract newContractAddress) + (try! (contract-call? oldContract shutdown-contract block-height)) + (try! (contract-call? newContract set-city-wallet (var-get cityWallet))) + (ok true) + ) +) + +(define-public (execute-upgrade-core-contract-job (jobId uint) (oldContract ) (newContract )) + (let + ( + (oldContractArg (unwrap! (get-principal-value-by-name jobId "oldContract") ERR_UNKNOWN_ARGUMENT)) + (newContractArg (unwrap! (get-principal-value-by-name jobId "newContract") ERR_UNKNOWN_ARGUMENT)) + (oldContractAddress (contract-of oldContract)) + (oldContractMap (unwrap! (map-get? CoreContracts oldContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + (newContractAddress (contract-of newContract)) + ) + (asserts! (not (is-eq oldContractAddress newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (is-none (map-get? CoreContracts newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (and (is-eq oldContractArg oldContractAddress) (is-eq newContractArg newContractAddress)) ERR_UNAUTHORIZED) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (map-set CoreContracts + oldContractAddress + { + state: STATE_INACTIVE, + startHeight: (get startHeight oldContractMap), + endHeight: block-height + }) + (map-set CoreContracts + newContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (var-set activeCoreContract newContractAddress) + (try! (contract-call? oldContract shutdown-contract block-height)) + (try! (contract-call? newContract set-city-wallet (var-get cityWallet))) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; CITY WALLET MANAGEMENT + +;; initial value for city wallet +(define-data-var cityWallet principal 'SM18VBF2QYAAHN57Q28E2HSM15F6078JZYZ2FQBCX) + +;; returns city wallet principal +(define-read-only (get-city-wallet) + (ok (var-get cityWallet)) +) + +;; protected function to update city wallet variable +(define-public (set-city-wallet (targetContract ) (newCityWallet principal)) + (let + ( + (coreContractAddress (contract-of targetContract)) + (coreContract (unwrap! (map-get? CoreContracts coreContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + ) + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + (asserts! (is-eq coreContractAddress (var-get activeCoreContract)) ERR_UNAUTHORIZED) + (var-set cityWallet newCityWallet) + (try! (contract-call? targetContract set-city-wallet newCityWallet)) + (ok true) + ) +) + +(define-public (execute-set-city-wallet-job (jobId uint) (targetContract )) + (let + ( + (coreContractAddress (contract-of targetContract)) + (coreContract (unwrap! (map-get? CoreContracts coreContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + (newCityWallet (unwrap! (get-principal-value-by-name jobId "newCityWallet") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (asserts! (is-eq coreContractAddress (var-get activeCoreContract)) ERR_UNAUTHORIZED) + (var-set cityWallet newCityWallet) + (try! (contract-call? targetContract set-city-wallet newCityWallet)) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; check if contract caller is city wallet +(define-private (is-authorized-city) + (is-eq contract-caller (var-get cityWallet)) +) + +;; TOKEN MANAGEMENT + +(define-public (set-token-uri (targetContract ) (newUri (optional (string-utf8 256)))) + (begin + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + (as-contract (try! (contract-call? targetContract set-token-uri newUri))) + (ok true) + ) +) + +;; COINBASE THRESHOLDS + +(define-public (update-coinbase-thresholds (targetCore ) (targetToken ) (threshold1 uint) (threshold2 uint) (threshold3 uint) (threshold4 uint) (threshold5 uint)) + (begin + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + ;; update in token contract + (as-contract (try! (contract-call? targetToken update-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5))) + ;; update core contract based on token contract + (as-contract (try! (contract-call? targetCore update-coinbase-thresholds))) + (ok true) + ) +) + +(define-public (execute-update-coinbase-thresholds-job (jobId uint) (targetCore ) (targetToken )) + (let + ( + (threshold1 (unwrap! (get-uint-value-by-name jobId "threshold1") ERR_UNKNOWN_ARGUMENT)) + (threshold2 (unwrap! (get-uint-value-by-name jobId "threshold2") ERR_UNKNOWN_ARGUMENT)) + (threshold3 (unwrap! (get-uint-value-by-name jobId "threshold3") ERR_UNKNOWN_ARGUMENT)) + (threshold4 (unwrap! (get-uint-value-by-name jobId "threshold4") ERR_UNKNOWN_ARGUMENT)) + (threshold5 (unwrap! (get-uint-value-by-name jobId "threshold5") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (as-contract (try! (contract-call? targetToken update-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5))) + (as-contract (try! (contract-call? targetCore update-coinbase-thresholds))) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; COINBASE AMOUNTS (REWARDS) + +(define-public (update-coinbase-amounts (targetCore ) (targetToken ) (amountBonus uint) (amount1 uint) (amount2 uint) (amount3 uint) (amount4 uint) (amount5 uint) (amountDefault uint)) + (begin + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + ;; update in token contract + (as-contract (try! (contract-call? targetToken update-coinbase-amounts amountBonus amount1 amount2 amount3 amount4 amount5 amountDefault))) + ;; update core contract based on token contract + (as-contract (try! (contract-call? targetCore update-coinbase-amounts))) + (ok true) + ) +) + +(define-public (execute-update-coinbase-amounts-job (jobId uint) (targetCore ) (targetToken )) + (let + ( + (amountBonus (unwrap! (get-uint-value-by-name jobId "amountBonus") ERR_UNKNOWN_ARGUMENT)) + (amount1 (unwrap! (get-uint-value-by-name jobId "amount1") ERR_UNKNOWN_ARGUMENT)) + (amount2 (unwrap! (get-uint-value-by-name jobId "amount2") ERR_UNKNOWN_ARGUMENT)) + (amount3 (unwrap! (get-uint-value-by-name jobId "amount3") ERR_UNKNOWN_ARGUMENT)) + (amount4 (unwrap! (get-uint-value-by-name jobId "amount4") ERR_UNKNOWN_ARGUMENT)) + (amount5 (unwrap! (get-uint-value-by-name jobId "amount5") ERR_UNKNOWN_ARGUMENT)) + (amountDefault (unwrap! (get-uint-value-by-name jobId "amountDefault") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (as-contract (try! (contract-call? targetToken update-coinbase-amounts amountBonus amount1 amount2 amount3 amount4 amount5 amountDefault))) + (as-contract (try! (contract-call? targetCore update-coinbase-amounts))) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; APPROVERS MANAGEMENT + +(define-public (execute-replace-approver-job (jobId uint)) + (let + ( + (oldApprover (unwrap! (get-principal-value-by-name jobId "oldApprover") ERR_UNKNOWN_ARGUMENT)) + (newApprover (unwrap! (get-principal-value-by-name jobId "newApprover") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (map-set Approvers oldApprover false) + (map-set Approvers newApprover true) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; CONTRACT INITIALIZATION + +(map-insert Approvers 'SP372JVX6EWE2M0XPA84MWZYRRG2M6CAC4VVC12V1 true) +(map-insert Approvers 'SP2R0DQYR7XHD161SH2GK49QRP1YSV7HE9JSG7W6G true) +(map-insert Approvers 'SPN4Y5QPGQA8882ZXW90ADC2DHYXMSTN8VAR8C3X true) +(map-insert Approvers 'SP3YYGCGX1B62CYAH4QX7PQE63YXG7RDTXD8BQHJQ true) +(map-insert Approvers 'SP7DGES13508FHRWS1FB0J3SZA326FP6QRMB6JDE true) diff --git a/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-auth-v2.json b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-auth-v2.json new file mode 100644 index 0000000..89c8a70 --- /dev/null +++ b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-auth-v2.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-core-v2.clar b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-core-v2.clar new file mode 100644 index 0000000..831a993 --- /dev/null +++ b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-core-v2.clar @@ -0,0 +1,1008 @@ +;; NEWYORKCITYCOIN CORE CONTRACT V2 +;; CityCoins Protocol Version 2.0.0 + +;; GENERAL CONFIGURATION + +(impl-trait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.citycoin-core) +(impl-trait 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-core-v2-trait.citycoin-core-v2) +(define-constant CONTRACT_OWNER tx-sender) + +;; ERROR CODES + +(define-constant ERR_UNAUTHORIZED (err u1000)) +(define-constant ERR_USER_ALREADY_REGISTERED (err u1001)) +(define-constant ERR_USER_NOT_FOUND (err u1002)) +(define-constant ERR_USER_ID_NOT_FOUND (err u1003)) +(define-constant ERR_ACTIVATION_THRESHOLD_REACHED (err u1004)) +(define-constant ERR_CONTRACT_NOT_ACTIVATED (err u1005)) +(define-constant ERR_USER_ALREADY_MINED (err u1006)) +(define-constant ERR_INSUFFICIENT_COMMITMENT (err u1007)) +(define-constant ERR_INSUFFICIENT_BALANCE (err u1008)) +(define-constant ERR_USER_DID_NOT_MINE_IN_BLOCK (err u1009)) +(define-constant ERR_CLAIMED_BEFORE_MATURITY (err u1010)) +(define-constant ERR_NO_MINERS_AT_BLOCK (err u1011)) +(define-constant ERR_REWARD_ALREADY_CLAIMED (err u1012)) +(define-constant ERR_MINER_DID_NOT_WIN (err u1013)) +(define-constant ERR_NO_VRF_SEED_FOUND (err u1014)) +(define-constant ERR_STACKING_NOT_AVAILABLE (err u1015)) +(define-constant ERR_CANNOT_STACK (err u1016)) +(define-constant ERR_REWARD_CYCLE_NOT_COMPLETED (err u1017)) +(define-constant ERR_NOTHING_TO_REDEEM (err u1018)) +(define-constant ERR_UNABLE_TO_FIND_CITY_WALLET (err u1019)) +(define-constant ERR_CLAIM_IN_WRONG_CONTRACT (err u1020)) +(define-constant ERR_BLOCK_HEIGHT_IN_PAST (err u1021)) +(define-constant ERR_COINBASE_AMOUNTS_NOT_FOUND (err u1022)) + +;; CITY WALLET MANAGEMENT + +;; initial value for city wallet, set to this contract until initialized +(define-data-var cityWallet principal 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-core-v2) + +;; returns set city wallet principal +(define-read-only (get-city-wallet) + (var-get cityWallet) +) + +;; protected function to update city wallet variable +(define-public (set-city-wallet (newCityWallet principal)) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (ok (var-set cityWallet newCityWallet)) + ) +) + +;; REGISTRATION + +(define-constant NEWYORKCITYCOIN_ACTIVATION_HEIGHT u37449) +(define-data-var activationBlock uint u340282366920938463463374607431768211455) +(define-data-var activationDelay uint u0) +(define-data-var activationReached bool false) +(define-data-var activationTarget uint u0) +(define-data-var activationThreshold uint u20) +(define-data-var usersNonce uint u0) + +;; returns Stacks block height registration was activated at plus activationDelay +(define-read-only (get-activation-block) + (begin + (asserts! (get-activation-status) ERR_CONTRACT_NOT_ACTIVATED) + (ok (var-get activationBlock)) + ) +) + +;; returns activation delay +(define-read-only (get-activation-delay) + (var-get activationDelay) +) + +;; returns activation status as boolean +(define-read-only (get-activation-status) + (var-get activationReached) +) + +;; returns activation target +(define-read-only (get-activation-target) + (begin + (asserts! (get-activation-status) ERR_CONTRACT_NOT_ACTIVATED) + (ok (var-get activationTarget)) + ) +) + +;; returns activation threshold +(define-read-only (get-activation-threshold) + (var-get activationThreshold) +) + +;; returns number of registered users, used for activation and tracking user IDs +(define-read-only (get-registered-users-nonce) + (var-get usersNonce) +) + +;; store user principal by user id +(define-map Users + uint + principal +) + +;; store user id by user principal +(define-map UserIds + principal + uint +) + +;; returns (some userId) or none +(define-read-only (get-user-id (user principal)) + (map-get? UserIds user) +) + +;; returns (some userPrincipal) or none +(define-read-only (get-user (userId uint)) + (map-get? Users userId) +) + +;; returns user ID if it has been created, or creates and returns new ID +(define-private (get-or-create-user-id (user principal)) + (match + (map-get? UserIds user) + value value + (let + ( + (newId (+ u1 (var-get usersNonce))) + ) + (map-set Users newId user) + (map-set UserIds user newId) + (var-set usersNonce newId) + newId + ) + ) +) + +;; registers users that signal activation of contract until threshold is met +(define-public (register-user (memo (optional (string-utf8 50)))) + (let + ( + (newId (+ u1 (var-get usersNonce))) + (threshold (var-get activationThreshold)) + (initialized (contract-call? 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-auth-v2 is-initialized)) + ) + + (asserts! initialized ERR_UNAUTHORIZED) + + (asserts! (is-none (map-get? UserIds tx-sender)) + ERR_USER_ALREADY_REGISTERED) + + (asserts! (<= newId threshold) + ERR_ACTIVATION_THRESHOLD_REACHED) + + (if (is-some memo) + (print memo) + none + ) + + (get-or-create-user-id tx-sender) + + (if (is-eq newId threshold) + (let + ( + (activationTargetBlock (+ block-height (var-get activationDelay))) + ) + (try! (contract-call? 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-auth-v2 activate-core-contract (as-contract tx-sender) activationTargetBlock)) + (try! (contract-call? 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 activate-token (as-contract tx-sender) NEWYORKCITYCOIN_ACTIVATION_HEIGHT)) + (try! (set-coinbase-thresholds)) + (try! (set-coinbase-amounts)) + (var-set activationReached true) + (var-set activationBlock NEWYORKCITYCOIN_ACTIVATION_HEIGHT) + (var-set activationTarget activationTargetBlock) + (ok true) + ) + (ok true) + ) + ) +) + +;; MINING CONFIGURATION + +;; define split to custodied wallet address for the city +(define-constant SPLIT_CITY_PCT u30) + +;; how long a miner must wait before block winner can claim their minted tokens +(define-data-var tokenRewardMaturity uint u100) + +;; At a given Stacks block height: +;; - how many miners were there +;; - what was the total amount submitted +;; - what was the total amount submitted to the city +;; - what was the total amount submitted to Stackers +;; - was the block reward claimed +(define-map MiningStatsAtBlock + uint + { + minersCount: uint, + amount: uint, + amountToCity: uint, + amountToStackers: uint, + rewardClaimed: bool + } +) + +;; returns map MiningStatsAtBlock at a given Stacks block height if it exists +(define-read-only (get-mining-stats-at-block (stacksHeight uint)) + (map-get? MiningStatsAtBlock stacksHeight) +) + +;; returns map MiningStatsAtBlock at a given Stacks block height +;; or, an empty structure +(define-read-only (get-mining-stats-at-block-or-default (stacksHeight uint)) + (default-to { + minersCount: u0, + amount: u0, + amountToCity: u0, + amountToStackers: u0, + rewardClaimed: false + } + (map-get? MiningStatsAtBlock stacksHeight) + ) +) + +;; At a given Stacks block height and user ID: +;; - what is their ustx commitment +;; - what are the low/high values (used for VRF) +(define-map MinersAtBlock + { + stacksHeight: uint, + userId: uint + } + { + ustx: uint, + lowValue: uint, + highValue: uint, + winner: bool + } +) + +;; returns true if a given miner has already mined at a given block height +(define-read-only (has-mined-at-block (stacksHeight uint) (userId uint)) + (is-some + (map-get? MinersAtBlock { stacksHeight: stacksHeight, userId: userId }) + ) +) + +;; returns map MinersAtBlock at a given Stacks block height for a user ID +(define-read-only (get-miner-at-block (stacksHeight uint) (userId uint)) + (map-get? MinersAtBlock { stacksHeight: stacksHeight, userId: userId }) +) + +;; returns map MinersAtBlock at a given Stacks block height for a user ID +;; or, an empty structure +(define-read-only (get-miner-at-block-or-default (stacksHeight uint) (userId uint)) + (default-to { + highValue: u0, + lowValue: u0, + ustx: u0, + winner: false + } + (map-get? MinersAtBlock { stacksHeight: stacksHeight, userId: userId })) +) + +;; At a given Stacks block height: +;; - what is the max highValue from MinersAtBlock (used for VRF) +(define-map MinersAtBlockHighValue + uint + uint +) + +;; returns last high value from map MinersAtBlockHighValue +(define-read-only (get-last-high-value-at-block (stacksHeight uint)) + (default-to u0 + (map-get? MinersAtBlockHighValue stacksHeight)) +) + +;; At a given Stacks block height: +;; - what is the userId of miner who won this block +(define-map BlockWinnerIds + uint + uint +) + +(define-read-only (get-block-winner-id (stacksHeight uint)) + (map-get? BlockWinnerIds stacksHeight) +) + +;; MINING ACTIONS + +(define-public (mine-tokens (amountUstx uint) (memo (optional (buff 34)))) + (let + ( + (userId (get-or-create-user-id tx-sender)) + ) + (try! (mine-tokens-at-block userId block-height amountUstx memo)) + (ok true) + ) +) + +(define-public (mine-many (amounts (list 200 uint))) + (begin + (asserts! (is-activated) ERR_CONTRACT_NOT_ACTIVATED) + (asserts! (> (len amounts) u0) ERR_INSUFFICIENT_COMMITMENT) + (match (fold mine-single amounts (ok { userId: (get-or-create-user-id tx-sender), toStackers: u0, toCity: u0, stacksHeight: block-height })) + okReturn + (begin + (asserts! (>= (stx-get-balance tx-sender) (+ (get toStackers okReturn) (get toCity okReturn))) ERR_INSUFFICIENT_BALANCE) + (if (> (get toStackers okReturn ) u0) + (try! (stx-transfer? (get toStackers okReturn ) tx-sender (as-contract tx-sender))) + false + ) + (try! (stx-transfer? (get toCity okReturn) tx-sender (var-get cityWallet))) + (print { + firstBlock: block-height, + lastBlock: (- (+ block-height (len amounts)) u1) + }) + (ok true) + ) + errReturn (err errReturn) + ) + ) +) + +(define-private (mine-single + (amountUstx uint) + (return (response + { + userId: uint, + toStackers: uint, + toCity: uint, + stacksHeight: uint + } + uint + ))) + + (match return okReturn + (let + ( + (stacksHeight (get stacksHeight okReturn)) + (rewardCycle (default-to u0 (get-reward-cycle stacksHeight))) + (stackingActive (stacking-active-at-cycle rewardCycle)) + (toCity + (if stackingActive + (/ (* SPLIT_CITY_PCT amountUstx) u100) + amountUstx + ) + ) + (toStackers (- amountUstx toCity)) + ) + (asserts! (not (has-mined-at-block stacksHeight (get userId okReturn))) ERR_USER_ALREADY_MINED) + (asserts! (> amountUstx u0) ERR_INSUFFICIENT_COMMITMENT) + (try! (set-tokens-mined (get userId okReturn) stacksHeight amountUstx toStackers toCity)) + (ok (merge okReturn + { + toStackers: (+ (get toStackers okReturn) toStackers), + toCity: (+ (get toCity okReturn) toCity), + stacksHeight: (+ stacksHeight u1) + } + )) + ) + errReturn (err errReturn) + ) +) + +(define-private (mine-tokens-at-block (userId uint) (stacksHeight uint) (amountUstx uint) (memo (optional (buff 34)))) + (let + ( + (rewardCycle (default-to u0 (get-reward-cycle stacksHeight))) + (stackingActive (stacking-active-at-cycle rewardCycle)) + (toCity + (if stackingActive + (/ (* SPLIT_CITY_PCT amountUstx) u100) + amountUstx + ) + ) + (toStackers (- amountUstx toCity)) + ) + (asserts! (is-activated) ERR_CONTRACT_NOT_ACTIVATED) + (asserts! (not (has-mined-at-block stacksHeight userId)) ERR_USER_ALREADY_MINED) + (asserts! (> amountUstx u0) ERR_INSUFFICIENT_COMMITMENT) + (asserts! (>= (stx-get-balance tx-sender) amountUstx) ERR_INSUFFICIENT_BALANCE) + (try! (set-tokens-mined userId stacksHeight amountUstx toStackers toCity)) + (if (is-some memo) + (print memo) + none + ) + (if stackingActive + (try! (stx-transfer? toStackers tx-sender (as-contract tx-sender))) + false + ) + (try! (stx-transfer? toCity tx-sender (var-get cityWallet))) + (ok true) + ) +) + +(define-private (set-tokens-mined (userId uint) (stacksHeight uint) (amountUstx uint) (toStackers uint) (toCity uint)) + (let + ( + (blockStats (get-mining-stats-at-block-or-default stacksHeight)) + (newMinersCount (+ (get minersCount blockStats) u1)) + (minerLowVal (get-last-high-value-at-block stacksHeight)) + (rewardCycle (unwrap! (get-reward-cycle stacksHeight) + ERR_STACKING_NOT_AVAILABLE)) + (rewardCycleStats (get-stacking-stats-at-cycle-or-default rewardCycle)) + ) + (map-set MiningStatsAtBlock + stacksHeight + { + minersCount: newMinersCount, + amount: (+ (get amount blockStats) amountUstx), + amountToCity: (+ (get amountToCity blockStats) toCity), + amountToStackers: (+ (get amountToStackers blockStats) toStackers), + rewardClaimed: false + } + ) + (map-set MinersAtBlock + { + stacksHeight: stacksHeight, + userId: userId + } + { + ustx: amountUstx, + lowValue: (if (> minerLowVal u0) (+ minerLowVal u1) u0), + highValue: (+ minerLowVal amountUstx), + winner: false + } + ) + (map-set MinersAtBlockHighValue + stacksHeight + (+ minerLowVal amountUstx) + ) + (if (> toStackers u0) + (map-set StackingStatsAtCycle + rewardCycle + { + amountUstx: (+ (get amountUstx rewardCycleStats) toStackers), + amountToken: (get amountToken rewardCycleStats) + } + ) + false + ) + (ok true) + ) +) + +;; MINING REWARD CLAIM ACTIONS + +;; calls function to claim mining reward in active logic contract +(define-public (claim-mining-reward (minerBlockHeight uint)) + (begin + (asserts! (or (is-eq (var-get shutdownHeight) u0) (< minerBlockHeight (var-get shutdownHeight))) ERR_CLAIM_IN_WRONG_CONTRACT) + (try! (claim-mining-reward-at-block tx-sender block-height minerBlockHeight)) + (ok true) + ) +) + +;; Determine whether or not the given principal can claim the mined tokens at a particular block height, +;; given the miners record for that block height, a random sample, and the current block height. +(define-private (claim-mining-reward-at-block (user principal) (stacksHeight uint) (minerBlockHeight uint)) + (let + ( + (maturityHeight (+ (var-get tokenRewardMaturity) minerBlockHeight)) + (userId (unwrap! (get-user-id user) ERR_USER_ID_NOT_FOUND)) + (blockStats (unwrap! (get-mining-stats-at-block minerBlockHeight) ERR_NO_MINERS_AT_BLOCK)) + (minerStats (unwrap! (get-miner-at-block minerBlockHeight userId) ERR_USER_DID_NOT_MINE_IN_BLOCK)) + (isMature (asserts! (> stacksHeight maturityHeight) ERR_CLAIMED_BEFORE_MATURITY)) + (vrfSample (unwrap! (contract-call? 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-vrf-v2 get-save-rnd maturityHeight) ERR_NO_VRF_SEED_FOUND)) + (commitTotal (get-last-high-value-at-block minerBlockHeight)) + (winningValue (mod vrfSample commitTotal)) + ) + (asserts! (not (get rewardClaimed blockStats)) ERR_REWARD_ALREADY_CLAIMED) + (asserts! (and (>= winningValue (get lowValue minerStats)) (<= winningValue (get highValue minerStats))) + ERR_MINER_DID_NOT_WIN) + (try! (set-mining-reward-claimed userId minerBlockHeight)) + (ok true) + ) +) + +(define-private (set-mining-reward-claimed (userId uint) (minerBlockHeight uint)) + (let + ( + (blockStats (get-mining-stats-at-block-or-default minerBlockHeight)) + (minerStats (get-miner-at-block-or-default minerBlockHeight userId)) + (user (unwrap! (get-user userId) ERR_USER_NOT_FOUND)) + ) + (map-set MiningStatsAtBlock + minerBlockHeight + { + minersCount: (get minersCount blockStats), + amount: (get amount blockStats), + amountToCity: (get amountToCity blockStats), + amountToStackers: (get amountToStackers blockStats), + rewardClaimed: true + } + ) + (map-set MinersAtBlock + { + stacksHeight: minerBlockHeight, + userId: userId + } + { + ustx: (get ustx minerStats), + lowValue: (get lowValue minerStats), + highValue: (get highValue minerStats), + winner: true + } + ) + (map-set BlockWinnerIds + minerBlockHeight + userId + ) + (try! (mint-coinbase user minerBlockHeight)) + (ok true) + ) +) + +(define-read-only (is-block-winner (user principal) (minerBlockHeight uint)) + (is-block-winner-and-can-claim user minerBlockHeight false) +) + +(define-read-only (can-claim-mining-reward (user principal) (minerBlockHeight uint)) + (is-block-winner-and-can-claim user minerBlockHeight true) +) + +(define-private (is-block-winner-and-can-claim (user principal) (minerBlockHeight uint) (testCanClaim bool)) + (let + ( + (userId (unwrap! (get-user-id user) false)) + (blockStats (unwrap! (get-mining-stats-at-block minerBlockHeight) false)) + (minerStats (unwrap! (get-miner-at-block minerBlockHeight userId) false)) + (maturityHeight (+ (var-get tokenRewardMaturity) minerBlockHeight)) + (vrfSample (unwrap! (contract-call? 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-vrf-v2 get-rnd maturityHeight) false)) + (commitTotal (get-last-high-value-at-block minerBlockHeight)) + (winningValue (mod vrfSample commitTotal)) + ) + (if (and (>= winningValue (get lowValue minerStats)) (<= winningValue (get highValue minerStats))) + (if testCanClaim (not (get rewardClaimed blockStats)) true) + false + ) + ) +) + +;; STACKING CONFIGURATION + +(define-constant MAX_REWARD_CYCLES u32) +(define-constant REWARD_CYCLE_INDEXES (list u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11 u12 u13 u14 u15 u16 u17 u18 u19 u20 u21 u22 u23 u24 u25 u26 u27 u28 u29 u30 u31)) + +;; how long a reward cycle is +(define-data-var rewardCycleLength uint u2100) + +;; At a given reward cycle: +;; - how many Stackers were there +;; - what is the total uSTX submitted by miners +;; - what is the total amount of tokens stacked +(define-map StackingStatsAtCycle + uint + { + amountUstx: uint, + amountToken: uint + } +) + +;; returns the total stacked tokens and committed uSTX for a given reward cycle +(define-read-only (get-stacking-stats-at-cycle (rewardCycle uint)) + (map-get? StackingStatsAtCycle rewardCycle) +) + +;; returns the total stacked tokens and committed uSTX for a given reward cycle +;; or, an empty structure +(define-read-only (get-stacking-stats-at-cycle-or-default (rewardCycle uint)) + (default-to { amountUstx: u0, amountToken: u0 } + (map-get? StackingStatsAtCycle rewardCycle)) +) + +;; At a given reward cycle and user ID: +;; - what is the total tokens Stacked? +;; - how many tokens should be returned? (based on Stacking period) +(define-map StackerAtCycle + { + rewardCycle: uint, + userId: uint + } + { + amountStacked: uint, + toReturn: uint + } +) + +(define-read-only (get-stacker-at-cycle (rewardCycle uint) (userId uint)) + (map-get? StackerAtCycle { rewardCycle: rewardCycle, userId: userId }) +) + +(define-read-only (get-stacker-at-cycle-or-default (rewardCycle uint) (userId uint)) + (default-to { amountStacked: u0, toReturn: u0 } + (map-get? StackerAtCycle { rewardCycle: rewardCycle, userId: userId })) +) + +;; get the reward cycle for a given Stacks block height +(define-read-only (get-reward-cycle (stacksHeight uint)) + (let + ( + (firstStackingBlock (var-get activationBlock)) + (rcLen (var-get rewardCycleLength)) + ) + (if (>= stacksHeight firstStackingBlock) + (some (/ (- stacksHeight firstStackingBlock) rcLen)) + none) + ) +) + +;; determine if stacking is active in a given cycle +(define-read-only (stacking-active-at-cycle (rewardCycle uint)) + (is-some + (get amountToken (map-get? StackingStatsAtCycle rewardCycle)) + ) +) + +;; get the first Stacks block height for a given reward cycle. +(define-read-only (get-first-stacks-block-in-reward-cycle (rewardCycle uint)) + (+ (var-get activationBlock) (* (var-get rewardCycleLength) rewardCycle)) +) + +;; getter for get-entitled-stacking-reward that specifies block height +(define-read-only (get-stacking-reward (userId uint) (targetCycle uint)) + (get-entitled-stacking-reward userId targetCycle block-height) +) + +;; get uSTX a Stacker can claim, given reward cycle they stacked in and current block height +;; this method only returns a positive value if: +;; - the current block height is in a subsequent reward cycle +;; - the stacker actually locked up tokens in the target reward cycle +;; - the stacker locked up _enough_ tokens to get at least one uSTX +;; it is possible to Stack tokens and not receive uSTX: +;; - if no miners commit during this reward cycle +;; - the amount stacked by user is too few that you'd be entitled to less than 1 uSTX +(define-private (get-entitled-stacking-reward (userId uint) (targetCycle uint) (stacksHeight uint)) + (let + ( + (rewardCycleStats (get-stacking-stats-at-cycle-or-default targetCycle)) + (stackerAtCycle (get-stacker-at-cycle-or-default targetCycle userId)) + (totalUstxThisCycle (get amountUstx rewardCycleStats)) + (totalStackedThisCycle (get amountToken rewardCycleStats)) + (userStackedThisCycle (get amountStacked stackerAtCycle)) + ) + (match (get-reward-cycle stacksHeight) + currentCycle + (if (and (not (var-get isShutdown)) + (or (<= currentCycle targetCycle) (is-eq u0 userStackedThisCycle))) + ;; the contract is not shut down and + ;; this cycle hasn't finished + ;; or stacker contributed nothing + u0 + ;; (totalUstxThisCycle * userStackedThisCycle) / totalStackedThisCycle + (/ (* totalUstxThisCycle userStackedThisCycle) totalStackedThisCycle) + ) + ;; before first reward cycle + u0 + ) + ) +) + +;; STACKING ACTIONS + +(define-public (stack-tokens (amountTokens uint) (lockPeriod uint)) + (let + ( + (userId (get-or-create-user-id tx-sender)) + ) + (try! (stack-tokens-at-cycle tx-sender userId amountTokens block-height lockPeriod)) + (ok true) + ) +) + +(define-private (stack-tokens-at-cycle (user principal) (userId uint) (amountTokens uint) (startHeight uint) (lockPeriod uint)) + (let + ( + (currentCycle (unwrap! (get-reward-cycle startHeight) ERR_STACKING_NOT_AVAILABLE)) + (targetCycle (+ u1 currentCycle)) + (commitment { + stackerId: userId, + amount: amountTokens, + first: targetCycle, + last: (+ targetCycle lockPeriod) + }) + ) + (asserts! (is-activated) ERR_CONTRACT_NOT_ACTIVATED) + (asserts! (and (> lockPeriod u0) (<= lockPeriod MAX_REWARD_CYCLES)) + ERR_CANNOT_STACK) + (asserts! (> amountTokens u0) ERR_CANNOT_STACK) + (try! (contract-call? 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 transfer amountTokens tx-sender (as-contract tx-sender) none)) + (print { + firstCycle: targetCycle, + lastCycle: (- (+ targetCycle lockPeriod) u1) + }) + (match (fold stack-tokens-closure REWARD_CYCLE_INDEXES (ok commitment)) + okValue (ok true) + errValue (err errValue) + ) + ) +) + +(define-private (stack-tokens-closure (rewardCycleIdx uint) + (commitmentResponse (response + { + stackerId: uint, + amount: uint, + first: uint, + last: uint + } + uint + ))) + + (match commitmentResponse + commitment + (let + ( + (stackerId (get stackerId commitment)) + (amountToken (get amount commitment)) + (firstCycle (get first commitment)) + (lastCycle (get last commitment)) + (targetCycle (+ firstCycle rewardCycleIdx)) + ) + (begin + (if (and (>= targetCycle firstCycle) (< targetCycle lastCycle)) + (begin + (if (is-eq targetCycle (- lastCycle u1)) + (set-tokens-stacked stackerId targetCycle amountToken amountToken) + (set-tokens-stacked stackerId targetCycle amountToken u0) + ) + true + ) + false + ) + commitmentResponse + ) + ) + errValue commitmentResponse + ) +) + +(define-private (set-tokens-stacked (userId uint) (targetCycle uint) (amountStacked uint) (toReturn uint)) + (let + ( + (rewardCycleStats (get-stacking-stats-at-cycle-or-default targetCycle)) + (stackerAtCycle (get-stacker-at-cycle-or-default targetCycle userId)) + ) + (map-set StackingStatsAtCycle + targetCycle + { + amountUstx: (get amountUstx rewardCycleStats), + amountToken: (+ amountStacked (get amountToken rewardCycleStats)) + } + ) + (map-set StackerAtCycle + { + rewardCycle: targetCycle, + userId: userId + } + { + amountStacked: (+ amountStacked (get amountStacked stackerAtCycle)), + toReturn: (+ toReturn (get toReturn stackerAtCycle)) + } + ) + ) +) + +;; STACKING REWARD CLAIMS + +;; calls function to claim stacking reward in active logic contract +(define-public (claim-stacking-reward (targetCycle uint)) + (begin + (try! (claim-stacking-reward-at-cycle tx-sender block-height targetCycle)) + (ok true) + ) +) + +(define-private (claim-stacking-reward-at-cycle (user principal) (stacksHeight uint) (targetCycle uint)) + (let + ( + (currentCycle (unwrap! (get-reward-cycle stacksHeight) ERR_STACKING_NOT_AVAILABLE)) + (userId (unwrap! (get-user-id user) ERR_USER_ID_NOT_FOUND)) + (entitledUstx (get-entitled-stacking-reward userId targetCycle stacksHeight)) + (stackerAtCycle (get-stacker-at-cycle-or-default targetCycle userId)) + (toReturn (get toReturn stackerAtCycle)) + ) + (asserts! (or + (is-eq true (var-get isShutdown)) + (> currentCycle targetCycle)) + ERR_REWARD_CYCLE_NOT_COMPLETED) + (asserts! (or (> toReturn u0) (> entitledUstx u0)) ERR_NOTHING_TO_REDEEM) + ;; disable ability to claim again + (map-set StackerAtCycle + { + rewardCycle: targetCycle, + userId: userId + } + { + amountStacked: u0, + toReturn: u0 + } + ) + ;; send back tokens if user was eligible + (if (> toReturn u0) + (try! (as-contract (contract-call? 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 transfer toReturn tx-sender user none))) + true + ) + ;; send back rewards if user was eligible + (if (> entitledUstx u0) + (try! (as-contract (stx-transfer? entitledUstx tx-sender user))) + true + ) + (ok true) + ) +) + +;; TOKEN CONFIGURATION + +;; decimals and multiplier for token +(define-constant DECIMALS u6) +(define-constant MICRO_CITYCOINS (pow u10 DECIMALS)) + +;; bonus period length for increased coinbase rewards +(define-constant TOKEN_BONUS_PERIOD u10000) + +;; coinbase thresholds per halving, used to determine halvings +(define-data-var coinbaseThreshold1 uint u0) +(define-data-var coinbaseThreshold2 uint u0) +(define-data-var coinbaseThreshold3 uint u0) +(define-data-var coinbaseThreshold4 uint u0) +(define-data-var coinbaseThreshold5 uint u0) + +;; return coinbase thresholds if contract activated +(define-read-only (get-coinbase-thresholds) + (let + ( + (activated (get-activation-status)) + ) + (asserts! activated ERR_CONTRACT_NOT_ACTIVATED) + (ok { + coinbaseThreshold1: (var-get coinbaseThreshold1), + coinbaseThreshold2: (var-get coinbaseThreshold2), + coinbaseThreshold3: (var-get coinbaseThreshold3), + coinbaseThreshold4: (var-get coinbaseThreshold4), + coinbaseThreshold5: (var-get coinbaseThreshold5) + }) + ) +) + +;; set coinbase thresholds, used during activation +(define-private (set-coinbase-thresholds) + (let + ( + (coinbaseThresholds (try! (contract-call? 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 get-coinbase-thresholds))) + ) + (var-set coinbaseThreshold1 (get coinbaseThreshold1 coinbaseThresholds)) + (var-set coinbaseThreshold2 (get coinbaseThreshold2 coinbaseThresholds)) + (var-set coinbaseThreshold3 (get coinbaseThreshold3 coinbaseThresholds)) + (var-set coinbaseThreshold4 (get coinbaseThreshold4 coinbaseThresholds)) + (var-set coinbaseThreshold5 (get coinbaseThreshold5 coinbaseThresholds)) + ;; print coinbase thresholds + (print { + coinbaseThreshold1: (var-get coinbaseThreshold1), + coinbaseThreshold2: (var-get coinbaseThreshold2), + coinbaseThreshold3: (var-get coinbaseThreshold3), + coinbaseThreshold4: (var-get coinbaseThreshold4), + coinbaseThreshold5: (var-get coinbaseThreshold5) + }) + (ok true) + ) +) + +;; guarded function for auth to update coinbase thresholds +(define-public (update-coinbase-thresholds) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (try! (set-coinbase-thresholds)) + (ok true) + ) +) + +;; coinbase rewards per threshold, used to determine rewards +(define-data-var coinbaseAmountBonus uint u0) +(define-data-var coinbaseAmount1 uint u0) +(define-data-var coinbaseAmount2 uint u0) +(define-data-var coinbaseAmount3 uint u0) +(define-data-var coinbaseAmount4 uint u0) +(define-data-var coinbaseAmount5 uint u0) +(define-data-var coinbaseAmountDefault uint u0) + +;; return coinbase amounts if contract activated +(define-read-only (get-coinbase-amounts) + (let + ( + (activated (get-activation-status)) + ) + (asserts! activated ERR_CONTRACT_NOT_ACTIVATED) + (ok { + coinbaseAmountBonus: (var-get coinbaseAmountBonus), + coinbaseAmount1: (var-get coinbaseAmount1), + coinbaseAmount2: (var-get coinbaseAmount2), + coinbaseAmount3: (var-get coinbaseAmount3), + coinbaseAmount4: (var-get coinbaseAmount4), + coinbaseAmount5: (var-get coinbaseAmount5), + coinbaseAmountDefault: (var-get coinbaseAmountDefault) + }) + ) +) + +;; set coinbase amounts, used during activation +(define-private (set-coinbase-amounts) + (let + ( + (coinbaseAmounts (unwrap! (contract-call? 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 get-coinbase-amounts) ERR_COINBASE_AMOUNTS_NOT_FOUND)) + ) + (var-set coinbaseAmountBonus (get coinbaseAmountBonus coinbaseAmounts)) + (var-set coinbaseAmount1 (get coinbaseAmount1 coinbaseAmounts)) + (var-set coinbaseAmount2 (get coinbaseAmount2 coinbaseAmounts)) + (var-set coinbaseAmount3 (get coinbaseAmount3 coinbaseAmounts)) + (var-set coinbaseAmount4 (get coinbaseAmount4 coinbaseAmounts)) + (var-set coinbaseAmount5 (get coinbaseAmount5 coinbaseAmounts)) + (var-set coinbaseAmountDefault (get coinbaseAmountDefault coinbaseAmounts)) + ;; print coinbase amounts + (print { + coinbaseAmountBonus: (var-get coinbaseAmountBonus), + coinbaseAmount1: (var-get coinbaseAmount1), + coinbaseAmount2: (var-get coinbaseAmount2), + coinbaseAmount3: (var-get coinbaseAmount3), + coinbaseAmount4: (var-get coinbaseAmount4), + coinbaseAmount5: (var-get coinbaseAmount5), + coinbaseAmountDefault: (var-get coinbaseAmountDefault) + }) + (ok true) + ) +) + +;; guarded function for auth to update coinbase amounts +(define-public (update-coinbase-amounts) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (try! (set-coinbase-amounts)) + (ok true) + ) +) + +;; function for deciding how many tokens to mint, depending on when they were mined +(define-read-only (get-coinbase-amount (minerBlockHeight uint)) + (begin + ;; if contract is not active, return 0 + (asserts! (>= minerBlockHeight (var-get activationBlock)) u0) + ;; if contract is active, return based on emissions schedule + ;; defined in CCIP-008 https://github.com/citycoins/governance + (asserts! (> minerBlockHeight (var-get coinbaseThreshold1)) + (if (<= (- minerBlockHeight (var-get activationBlock)) TOKEN_BONUS_PERIOD) + ;; bonus reward for initial miners + (var-get coinbaseAmountBonus) + ;; standard reward until 1st halving + (var-get coinbaseAmount1) + ) + ) + ;; computations based on each halving threshold + (asserts! (> minerBlockHeight (var-get coinbaseThreshold2)) (var-get coinbaseAmount2)) + (asserts! (> minerBlockHeight (var-get coinbaseThreshold3)) (var-get coinbaseAmount3)) + (asserts! (> minerBlockHeight (var-get coinbaseThreshold4)) (var-get coinbaseAmount4)) + (asserts! (> minerBlockHeight (var-get coinbaseThreshold5)) (var-get coinbaseAmount5)) + ;; default value after 5th halving + (var-get coinbaseAmountDefault) + ) +) + +;; mint new tokens for claimant who won at given Stacks block height +(define-private (mint-coinbase (recipient principal) (stacksHeight uint)) + (as-contract (contract-call? 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 mint (get-coinbase-amount stacksHeight) recipient)) +) + +;; UTILITIES + +(define-data-var shutdownHeight uint u0) +(define-data-var isShutdown bool false) + +;; stop mining and stacking operations +;; in preparation for a core upgrade +(define-public (shutdown-contract (stacksHeight uint)) + (begin + ;; make sure block height is in the future + (asserts! (>= stacksHeight block-height) ERR_BLOCK_HEIGHT_IN_PAST) + ;; only allow shutdown request from AUTH + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + ;; set variables to disable mining/stacking in CORE + (var-set activationReached false) + (var-set shutdownHeight stacksHeight) + ;; set variable to allow for all stacking claims + (var-set isShutdown true) + (ok true) + ) +) + +;; checks if caller is Auth contract +(define-private (is-authorized-auth) + (is-eq contract-caller 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-auth-v2) +) + +;; checks if contract is fully activated to +;; enable mining and stacking functions +(define-private (is-activated) + (and (get-activation-status) (>= block-height (var-get activationTarget))) +) diff --git a/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-core-v2.json b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-core-v2.json new file mode 100644 index 0000000..89c8a70 --- /dev/null +++ b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-core-v2.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2.clar b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2.clar new file mode 100644 index 0000000..63a6f60 --- /dev/null +++ b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2.clar @@ -0,0 +1,301 @@ +;; NEWYORKCITYCOIN TOKEN V2 CONTRACT +;; CityCoins Protocol Version 2.0.0 + +;; TRAIT DEFINITIONS + +(impl-trait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-token-trait.citycoin-token) +(impl-trait 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-token-v2-trait.citycoin-token-v2) +(use-trait coreTrait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.citycoin-core) + +;; ERROR CODES + +(define-constant ERR_UNAUTHORIZED (err u2000)) +(define-constant ERR_TOKEN_NOT_ACTIVATED (err u2001)) +(define-constant ERR_TOKEN_ALREADY_ACTIVATED (err u2002)) +(define-constant ERR_V1_BALANCE_NOT_FOUND (err u2003)) +(define-constant ERR_INVALID_COINBASE_THRESHOLD (err u2004)) +(define-constant ERR_INVALID_COINBASE_AMOUNT (err u2005)) + +;; SIP-010 DEFINITION + +(impl-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait) + +(define-fungible-token newyorkcitycoin) + +(define-constant DECIMALS u6) +(define-constant MICRO_CITYCOINS (pow u10 DECIMALS)) + +;; SIP-010 FUNCTIONS + +(define-public (transfer (amount uint) (from principal) (to principal) (memo (optional (buff 34)))) + (begin + (asserts! (is-eq from tx-sender) ERR_UNAUTHORIZED) + (if (is-some memo) + (print memo) + none + ) + (ft-transfer? newyorkcitycoin amount from to) + ) +) + +(define-read-only (get-name) + (ok "newyorkcitycoin") +) + +(define-read-only (get-symbol) + (ok "NYC") +) + +(define-read-only (get-decimals) + (ok DECIMALS) +) + +(define-read-only (get-balance (user principal)) + (ok (ft-get-balance newyorkcitycoin user)) +) + +(define-read-only (get-total-supply) + (ok (ft-get-supply newyorkcitycoin)) +) + +(define-read-only (get-token-uri) + (ok (var-get tokenUri)) +) + +;; TOKEN CONFIGURATION + +;; define bonus period and initial epoch length +(define-constant TOKEN_BONUS_PERIOD u10000) +(define-constant TOKEN_EPOCH_LENGTH u25000) + +;; once activated, activation cannot happen again +(define-data-var tokenActivated bool false) + +;; core contract states +(define-constant STATE_DEPLOYED u0) +(define-constant STATE_ACTIVE u1) +(define-constant STATE_INACTIVE u2) + +;; one-time function to activate the token +(define-public (activate-token (coreContract principal) (stacksHeight uint)) + (let + ( + (coreContractMap (try! (contract-call? 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-auth-v2 get-core-contract-info coreContract))) + (threshold1 (+ stacksHeight TOKEN_BONUS_PERIOD TOKEN_EPOCH_LENGTH)) ;; 35,000 blocks + (threshold2 (+ stacksHeight TOKEN_BONUS_PERIOD (* u3 TOKEN_EPOCH_LENGTH))) ;; 85,000 blocks + (threshold3 (+ stacksHeight TOKEN_BONUS_PERIOD (* u7 TOKEN_EPOCH_LENGTH))) ;; 185,000 blocks + (threshold4 (+ stacksHeight TOKEN_BONUS_PERIOD (* u15 TOKEN_EPOCH_LENGTH))) ;; 385,000 blocks + (threshold5 (+ stacksHeight TOKEN_BONUS_PERIOD (* u31 TOKEN_EPOCH_LENGTH))) ;; 785,000 blocks + ) + (asserts! (is-eq (get state coreContractMap) STATE_ACTIVE) ERR_UNAUTHORIZED) + (asserts! (not (var-get tokenActivated)) ERR_TOKEN_ALREADY_ACTIVATED) + (var-set tokenActivated true) + (try! (set-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5)) + (ok true) + ) +) + +;; COINBASE THRESHOLDS + +;; coinbase thresholds per halving, used to select coinbase rewards in core +;; initially set by register-user in core contract per CCIP-008 +(define-data-var coinbaseThreshold1 uint u0) +(define-data-var coinbaseThreshold2 uint u0) +(define-data-var coinbaseThreshold3 uint u0) +(define-data-var coinbaseThreshold4 uint u0) +(define-data-var coinbaseThreshold5 uint u0) + +;; return coinbase thresholds if token activated +(define-read-only (get-coinbase-thresholds) + (let + ( + (activated (var-get tokenActivated)) + ) + (asserts! activated ERR_TOKEN_NOT_ACTIVATED) + (ok { + coinbaseThreshold1: (var-get coinbaseThreshold1), + coinbaseThreshold2: (var-get coinbaseThreshold2), + coinbaseThreshold3: (var-get coinbaseThreshold3), + coinbaseThreshold4: (var-get coinbaseThreshold4), + coinbaseThreshold5: (var-get coinbaseThreshold5) + }) + ) +) + +(define-private (set-coinbase-thresholds (threshold1 uint) (threshold2 uint) (threshold3 uint) (threshold4 uint) (threshold5 uint)) + (begin + ;; check that all thresholds increase in value + (asserts! (and (> threshold1 u0) (> threshold2 threshold1) (> threshold3 threshold2) (> threshold4 threshold3) (> threshold5 threshold4)) ERR_INVALID_COINBASE_THRESHOLD) + ;; set coinbase thresholds + (var-set coinbaseThreshold1 threshold1) + (var-set coinbaseThreshold2 threshold2) + (var-set coinbaseThreshold3 threshold3) + (var-set coinbaseThreshold4 threshold4) + (var-set coinbaseThreshold5 threshold5) + ;; print coinbase thresholds + (print { + coinbaseThreshold1: threshold1, + coinbaseThreshold2: threshold2, + coinbaseThreshold3: threshold3, + coinbaseThreshold4: threshold4, + coinbaseThreshold5: threshold5 + }) + (ok true) + ) +) + +;; only accessible by auth +(define-public (update-coinbase-thresholds (threshold1 uint) (threshold2 uint) (threshold3 uint) (threshold4 uint) (threshold5 uint)) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (asserts! (var-get tokenActivated) ERR_TOKEN_NOT_ACTIVATED) + (try! (set-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5)) + (ok true) + ) +) + +;; COINBASE AMOUNTS (REWARDS) + +;; coinbase rewards per threshold per CCIP-008 +(define-data-var coinbaseAmountBonus uint (* MICRO_CITYCOINS u250000)) +(define-data-var coinbaseAmount1 uint (* MICRO_CITYCOINS u100000)) +(define-data-var coinbaseAmount2 uint (* MICRO_CITYCOINS u50000)) +(define-data-var coinbaseAmount3 uint (* MICRO_CITYCOINS u25000)) +(define-data-var coinbaseAmount4 uint (* MICRO_CITYCOINS u12500)) +(define-data-var coinbaseAmount5 uint (* MICRO_CITYCOINS u6250)) +(define-data-var coinbaseAmountDefault uint (* MICRO_CITYCOINS u3125)) + +;; return coinbase thresholds if token activated +(define-read-only (get-coinbase-amounts) + (ok { + coinbaseAmountBonus: (var-get coinbaseAmountBonus), + coinbaseAmount1: (var-get coinbaseAmount1), + coinbaseAmount2: (var-get coinbaseAmount2), + coinbaseAmount3: (var-get coinbaseAmount3), + coinbaseAmount4: (var-get coinbaseAmount4), + coinbaseAmount5: (var-get coinbaseAmount5), + coinbaseAmountDefault: (var-get coinbaseAmountDefault) + }) +) + +(define-private (set-coinbase-amounts (amountBonus uint) (amount1 uint) (amount2 uint) (amount3 uint) (amount4 uint) (amount5 uint) (amountDefault uint)) + (begin + ;; check that all amounts are greater than zero + (asserts! (and (> amountBonus u0) (> amount1 u0) (> amount2 u0) (> amount3 u0) (> amount4 u0) (> amount5 u0) (> amountDefault u0)) ERR_INVALID_COINBASE_AMOUNT) + ;; set coinbase amounts in token contract + (var-set coinbaseAmountBonus amountBonus) + (var-set coinbaseAmount1 amount1) + (var-set coinbaseAmount2 amount2) + (var-set coinbaseAmount3 amount3) + (var-set coinbaseAmount4 amount4) + (var-set coinbaseAmount5 amount5) + (var-set coinbaseAmountDefault amountDefault) + ;; print coinbase amounts + (print { + coinbaseAmountBonus: amountBonus, + coinbaseAmount1: amount1, + coinbaseAmount2: amount2, + coinbaseAmount3: amount3, + coinbaseAmount4: amount4, + coinbaseAmount5: amount5, + coinbaseAmountDefault: amountDefault + }) + (ok true) + ) +) + +;; only accessible by auth +(define-public (update-coinbase-amounts (amountBonus uint) (amount1 uint) (amount2 uint) (amount3 uint) (amount4 uint) (amount5 uint) (amountDefault uint)) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + ;; (asserts! (var-get tokenActivated) ERR_TOKEN_NOT_ACTIVATED) + (try! (set-coinbase-amounts amountBonus amount1 amount2 amount3 amount4 amount5 amountDefault)) + (ok true) + ) +) + +;; V1 TO V2 CONVERSION + +(define-public (convert-to-v2) + (let + ( + (balanceV1 (unwrap! (contract-call? 'SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token get-balance tx-sender) ERR_V1_BALANCE_NOT_FOUND)) + ) + ;; verify positive balance + (asserts! (> balanceV1 u0) ERR_V1_BALANCE_NOT_FOUND) + ;; burn old + (try! (contract-call? 'SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token burn balanceV1 tx-sender)) + (print { + burnedV1: balanceV1, + mintedV2: (* balanceV1 MICRO_CITYCOINS), + tx-sender: tx-sender, + contract-caller: contract-caller + }) + ;; create new + (ft-mint? newyorkcitycoin (* balanceV1 MICRO_CITYCOINS) tx-sender) + ) +) + +;; UTILITIES + +(define-data-var tokenUri (optional (string-utf8 256)) (some u"https://cdn.citycoins.co/metadata/newyorkcitycoin.json")) + +;; set token URI to new value, only accessible by Auth +(define-public (set-token-uri (newUri (optional (string-utf8 256)))) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (ok (var-set tokenUri newUri)) + ) +) + +;; mint new tokens, only accessible by a Core contract +(define-public (mint (amount uint) (recipient principal)) + (let + ( + (coreContract (try! (contract-call? 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-auth-v2 get-core-contract-info contract-caller))) + ) + (ft-mint? newyorkcitycoin amount recipient) + ) +) + +;; burn tokens +(define-public (burn (amount uint) (owner principal)) + (begin + (asserts! (is-eq tx-sender owner) ERR_UNAUTHORIZED) + (ft-burn? newyorkcitycoin amount owner) + ) +) + +;; checks if caller is Auth contract +(define-private (is-authorized-auth) + (is-eq contract-caller 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-auth-v2) +) + +;; SEND-MANY + +(define-public (send-many (recipients (list 200 { to: principal, amount: uint, memo: (optional (buff 34)) }))) + (fold check-err + (map send-token recipients) + (ok true) + ) +) + +(define-private (check-err (result (response bool uint)) (prior (response bool uint))) + (match prior ok-value + result + err-value (err err-value) + ) +) + +(define-private (send-token (recipient { to: principal, amount: uint, memo: (optional (buff 34)) })) + (send-token-with-memo (get amount recipient) (get to recipient) (get memo recipient)) +) + +(define-private (send-token-with-memo (amount uint) (to principal) (memo (optional (buff 34)))) + (let + ( + (transferOk (try! (transfer amount tx-sender to memo))) + ) + (ok transferOk) + ) +) diff --git a/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2.json b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2.json new file mode 100644 index 0000000..89c8a70 --- /dev/null +++ b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/ST000000000000000000002AMW42H.pox.clar b/.cache/requirements/ST000000000000000000002AMW42H.pox.clar new file mode 100644 index 0000000..ac1c532 --- /dev/null +++ b/.cache/requirements/ST000000000000000000002AMW42H.pox.clar @@ -0,0 +1,695 @@ +;; PoX 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 contract +;; Error codes +(define-constant ERR_STACKING_UNREACHABLE 255) +(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) + +;; PoX disabling threshold (a percent) +(define-constant POX_REJECTION_FRACTION u25) + +;; 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) + +;; 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 + (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 configured true) + (ok true)) +) + +;; The Stacking lock-up state and associated metadata. +;; Records can be inserted into this map via one of two ways: +;; * via contract-call? to the (stack-stx) method, or +;; * via a transaction in the underlying burnchain that encodes the same data. +;; In the latter case, this map will be updated by the Stacks +;; node itself, and transactions in the burnchain will take priority +;; over transactions in the Stacks chain when processing this block. +(define-map stacking-state + { stacker: principal } + { + ;; how many uSTX locked? + amount-ustx: uint, + ;; 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, or p2wsh-p2sh UTXO, depending on the version. + pox-addr: { version: (buff 1), hashbytes: (buff 20) }, + ;; how long the uSTX are locked, in reward cycles. + lock-period: uint, + ;; reward cycle when rewards begin + first-reward-cycle: uint + } +) + +;; 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 20) }) + } +) + +;; 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 20) }, + total-ustx: uint + } +) + +(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 20) }, + 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-private (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-private (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-private (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-private (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))) + ;; is the caller allowance expired? + (if (< burn-block-height (unwrap! (get until-burn-ht caller-allowed) true)) + false + true)))) + +(define-private (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-private (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! +(define-private (append-reward-cycle-pox-addr (pox-addr (tuple (version (buff 1)) (hashbytes (buff 20)))) + (reward-cycle uint) + (amount-ustx uint)) + (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 }) + (map-set reward-cycle-pox-address-list-len + { reward-cycle: reward-cycle } + { len: (+ u1 sz) }) + (+ u1 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 })) + +;; Add a PoX address to the ith reward cycle, if i is between 0 and the given num-cycles (exclusive). +;; Arguments are given as a tuple, so this function can be (map ..)'ed onto a list of its arguments. +;; Used by add-pox-addr-to-reward-cycles. +;; No checking is done. +;; Returns 1 if added. +;; Returns 0 if not added. +(define-private (add-pox-addr-to-ith-reward-cycle (cycle-index uint) (params (tuple + (pox-addr (tuple (version (buff 1)) (hashbytes (buff 20)))) + (first-reward-cycle uint) + (num-cycles uint) + (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))) + { + pox-addr: (get pox-addr params), + first-reward-cycle: (get first-reward-cycle params), + num-cycles: num-cycles, + amount-ustx: (get amount-ustx params), + i: (if (< i num-cycles) + (let ((total-ustx (get-total-ustx-stacked reward-cycle))) + ;; 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)) + + ;; update running total + (map-set reward-cycle-total-stacked + { reward-cycle: reward-cycle } + { total-ustx: (+ (get amount-ustx params) total-ustx) }) + + ;; updated _this_ reward cycle + (+ i u1)) + (+ i u0)) + })) + +;; 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 20)))) + (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))) + ;; 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 (fold add-pox-addr-to-ith-reward-cycle cycle-indexes + { pox-addr: pox-addr, first-reward-cycle: first-reward-cycle, num-cycles: num-cycles, amount-ustx: amount-ustx, i: u0 }))) + (err ERR_STACKING_UNREACHABLE)) + (ok true))) + +(define-private (add-pox-partial-stacked-to-ith-cycle + (cycle-index uint) + (params { pox-addr: { version: (buff 1), hashbytes: (buff 20) }, + 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 20)))) + (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 burn address? +(define-private (check-pox-addr-version (version (buff 1))) + (or (is-eq version ADDRESS_VERSION_P2PKH) + (is-eq version ADDRESS_VERSION_P2SH) + (is-eq version ADDRESS_VERSION_P2WPKH) + (is-eq version ADDRESS_VERSION_P2WSH))) + +;; Is the given lock period valid? +(define-private (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 20)))) + (amount-ustx uint) + (first-reward-cycle uint) + (num-cycles uint)) + (begin + ;; minimum uSTX must be met + (asserts! (<= (print (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 20)))) + (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)) + (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 20)))) + (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 + (try! (add-pox-addr-to-reward-cycles pox-addr first-reward-cycle lock-period amount-ustx)) + + ;; add stacker record + (map-set stacking-state + { stacker: tx-sender } + { amount-ustx: amount-ustx, + pox-addr: pox-addr, + first-reward-cycle: first-reward-cycle, + lock-period: lock-period }) + + ;; 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 expiration +;; * 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 20) }))) + (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)) + + ;; 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. +;; 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 +(define-public (stack-aggregation-commit (pox-addr { version: (buff 1), hashbytes: (buff 20) }) + (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 + (add-pox-addr-to-ith-reward-cycle + u0 + { pox-addr: pox-addr, + first-reward-cycle: reward-cycle, + num-cycles: u1, + amount-ustx: amount-ustx, + i: 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 + (map-delete partial-stacked-by-cycle { pox-addr: pox-addr, sender: tx-sender, reward-cycle: reward-cycle }) + (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 20) }) + (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 } + { amount-ustx: amount-ustx, + pox-addr: pox-addr, + first-reward-cycle: first-reward-cycle, + lock-period: lock-period }) + + ;; 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, + }) +) diff --git a/.cache/requirements/ST000000000000000000002AMW42H.pox.json b/.cache/requirements/ST000000000000000000002AMW42H.pox.json new file mode 100644 index 0000000..11162a1 --- /dev/null +++ b/.cache/requirements/ST000000000000000000002AMW42H.pox.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch20", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-auth-v2.clar b/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-auth-v2.clar new file mode 100644 index 0000000..792c615 --- /dev/null +++ b/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-auth-v2.clar @@ -0,0 +1,630 @@ +;; MIAMICOIN AUTH CONTRACT V2 TESTNET +;; CityCoins Protocol Version 2.0.0 + +(define-constant CONTRACT_OWNER tx-sender) + +;; TRAIT DEFINITIONS + +(use-trait coreTraitV2 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-core-v2-trait.citycoin-core-v2) +(use-trait tokenTraitV2 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-token-v2-trait.citycoin-token-v2) + +;; ERRORS + +(define-constant ERR_UNKNOWN_JOB (err u6000)) +(define-constant ERR_UNAUTHORIZED (err u6001)) +(define-constant ERR_JOB_IS_ACTIVE (err u6002)) +(define-constant ERR_JOB_IS_NOT_ACTIVE (err u6003)) +(define-constant ERR_ALREADY_VOTED_THIS_WAY (err u6004)) +(define-constant ERR_JOB_IS_EXECUTED (err u6005)) +(define-constant ERR_JOB_IS_NOT_APPROVED (err u6006)) +(define-constant ERR_ARGUMENT_ALREADY_EXISTS (err u6007)) +(define-constant ERR_NO_ACTIVE_CORE_CONTRACT (err u6008)) +(define-constant ERR_CORE_CONTRACT_NOT_FOUND (err u6009)) +(define-constant ERR_UNKNOWN_ARGUMENT (err u6010)) +(define-constant ERR_INCORRECT_CONTRACT_STATE (err u6011)) +(define-constant ERR_CONTRACT_ALREADY_EXISTS (err u6012)) + +;; JOB MANAGEMENT + +(define-constant REQUIRED_APPROVALS u2) + +(define-data-var lastJobId uint u0) + +(define-map Jobs + uint + { + creator: principal, + name: (string-ascii 255), + target: principal, + approvals: uint, + disapprovals: uint, + isActive: bool, + isExecuted: bool + } +) + +(define-map JobApprovers + { jobId: uint, approver: principal } + bool +) + +(define-map Approvers + principal + bool +) + +(define-map ArgumentLastIdsByType + { jobId: uint, argumentType: (string-ascii 25) } + uint +) + +(define-map UIntArgumentsByName + { jobId: uint, argumentName: (string-ascii 255) } + { argumentId: uint, value: uint} +) + +(define-map UIntArgumentsById + { jobId: uint, argumentId: uint } + { argumentName: (string-ascii 255), value: uint } +) + +(define-map PrincipalArgumentsByName + { jobId: uint, argumentName: (string-ascii 255) } + { argumentId: uint, value: principal } +) + +(define-map PrincipalArgumentsById + { jobId: uint, argumentId: uint } + { argumentName: (string-ascii 255), value: principal } +) + +;; FUNCTIONS + +(define-read-only (get-last-job-id) + (var-get lastJobId) +) + +(define-public (create-job (name (string-ascii 255)) (target principal)) + (let + ( + (newJobId (+ (var-get lastJobId) u1)) + ) + (asserts! (is-approver tx-sender) ERR_UNAUTHORIZED) + (map-set Jobs + newJobId + { + creator: tx-sender, + name: name, + target: target, + approvals: u0, + disapprovals: u0, + isActive: false, + isExecuted: false + } + ) + (var-set lastJobId newJobId) + (ok newJobId) + ) +) + +(define-read-only (get-job (jobId uint)) + (map-get? Jobs jobId) +) + +(define-public (activate-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + ) + (asserts! (is-eq (get creator job) tx-sender) ERR_UNAUTHORIZED) + (asserts! (not (get isActive job)) ERR_JOB_IS_ACTIVE) + (map-set Jobs + jobId + (merge job { isActive: true }) + ) + (ok true) + ) +) + +(define-public (approve-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + (previousVote (map-get? JobApprovers { jobId: jobId, approver: tx-sender })) + ) + (asserts! (get isActive job) ERR_JOB_IS_NOT_ACTIVE) + (asserts! (is-approver tx-sender) ERR_UNAUTHORIZED) + ;; save vote + (map-set JobApprovers + { jobId: jobId, approver: tx-sender } + true + ) + (match previousVote approved + (begin + (asserts! (not approved) ERR_ALREADY_VOTED_THIS_WAY) + (map-set Jobs jobId + (merge job + { + approvals: (+ (get approvals job) u1), + disapprovals: (- (get disapprovals job) u1) + } + ) + ) + ) + ;; no previous vote + (map-set Jobs + jobId + (merge job { approvals: (+ (get approvals job) u1) } ) + ) + ) + (ok true) + ) +) + +(define-public (disapprove-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + (previousVote (map-get? JobApprovers { jobId: jobId, approver: tx-sender })) + ) + (asserts! (get isActive job) ERR_JOB_IS_NOT_ACTIVE) + (asserts! (is-approver tx-sender) ERR_UNAUTHORIZED) + ;; save vote + (map-set JobApprovers + { jobId: jobId, approver: tx-sender } + false + ) + (match previousVote approved + (begin + (asserts! approved ERR_ALREADY_VOTED_THIS_WAY) + (map-set Jobs jobId + (merge job + { + approvals: (- (get approvals job) u1), + disapprovals: (+ (get disapprovals job) u1) + } + ) + ) + ) + ;; no previous vote + (map-set Jobs + jobId + (merge job { disapprovals: (+ (get disapprovals job) u1) } ) + ) + ) + (ok true) + ) +) + +(define-read-only (is-job-approved (jobId uint)) + (match (get-job jobId) job + (>= (get approvals job) REQUIRED_APPROVALS) + false + ) +) + +(define-public (mark-job-as-executed (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + ) + (asserts! (get isActive job) ERR_JOB_IS_NOT_ACTIVE) + (asserts! (>= (get approvals job) REQUIRED_APPROVALS) ERR_JOB_IS_NOT_APPROVED) + (asserts! (is-eq (get target job) contract-caller) ERR_UNAUTHORIZED) + (asserts! (not (get isExecuted job)) ERR_JOB_IS_EXECUTED) + (map-set Jobs + jobId + (merge job { isExecuted: true }) + ) + (ok true) + ) +) + +(define-public (add-uint-argument (jobId uint) (argumentName (string-ascii 255)) (value uint)) + (let + ( + (argumentId (generate-argument-id jobId "uint")) + ) + (try! (guard-add-argument jobId)) + (asserts! + (and + (map-insert UIntArgumentsById + { jobId: jobId, argumentId: argumentId } + { argumentName: argumentName, value: value } + ) + (map-insert UIntArgumentsByName + { jobId: jobId, argumentName: argumentName } + { argumentId: argumentId, value: value} + ) + ) + ERR_ARGUMENT_ALREADY_EXISTS + ) + (ok true) + ) +) + +(define-read-only (get-uint-argument-by-name (jobId uint) (argumentName (string-ascii 255))) + (map-get? UIntArgumentsByName { jobId: jobId, argumentName: argumentName }) +) + +(define-read-only (get-uint-argument-by-id (jobId uint) (argumentId uint)) + (map-get? UIntArgumentsById { jobId: jobId, argumentId: argumentId }) +) + +(define-read-only (get-uint-value-by-name (jobId uint) (argumentName (string-ascii 255))) + (get value (get-uint-argument-by-name jobId argumentName)) +) + +(define-read-only (get-uint-value-by-id (jobId uint) (argumentId uint)) + (get value (get-uint-argument-by-id jobId argumentId)) +) + +(define-public (add-principal-argument (jobId uint) (argumentName (string-ascii 255)) (value principal)) + (let + ( + (argumentId (generate-argument-id jobId "principal")) + ) + (try! (guard-add-argument jobId)) + (asserts! + (and + (map-insert PrincipalArgumentsById + { jobId: jobId, argumentId: argumentId } + { argumentName: argumentName, value: value } + ) + (map-insert PrincipalArgumentsByName + { jobId: jobId, argumentName: argumentName } + { argumentId: argumentId, value: value} + ) + ) + ERR_ARGUMENT_ALREADY_EXISTS + ) + (ok true) + ) +) + +(define-read-only (get-principal-argument-by-name (jobId uint) (argumentName (string-ascii 255))) + (map-get? PrincipalArgumentsByName { jobId: jobId, argumentName: argumentName }) +) + +(define-read-only (get-principal-argument-by-id (jobId uint) (argumentId uint)) + (map-get? PrincipalArgumentsById { jobId: jobId, argumentId: argumentId }) +) + +(define-read-only (get-principal-value-by-name (jobId uint) (argumentName (string-ascii 255))) + (get value (get-principal-argument-by-name jobId argumentName)) +) + +(define-read-only (get-principal-value-by-id (jobId uint) (argumentId uint)) + (get value (get-principal-argument-by-id jobId argumentId)) +) + +;; PRIVATE FUNCTIONS + +(define-read-only (is-approver (user principal)) + (default-to false (map-get? Approvers user)) +) + +(define-private (generate-argument-id (jobId uint) (argumentType (string-ascii 25))) + (let + ( + (argumentId (+ (default-to u0 (map-get? ArgumentLastIdsByType { jobId: jobId, argumentType: argumentType })) u1)) + ) + (map-set ArgumentLastIdsByType + { jobId: jobId, argumentType: argumentType } + argumentId + ) + ;; return + argumentId + ) +) + +(define-private (guard-add-argument (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + ) + (asserts! (not (get isActive job)) ERR_JOB_IS_ACTIVE) + (asserts! (is-eq (get creator job) contract-caller) ERR_UNAUTHORIZED) + (ok true) + ) +) + +;; CONTRACT MANAGEMENT + +;; initial value for active core contract +;; set to deployer address at startup to prevent +;; circular dependency of core on auth +(define-data-var activeCoreContract principal CONTRACT_OWNER) +(define-data-var initialized bool false) + +;; core contract states +(define-constant STATE_DEPLOYED u0) +(define-constant STATE_ACTIVE u1) +(define-constant STATE_INACTIVE u2) + +;; core contract map +(define-map CoreContracts + principal + { + state: uint, + startHeight: uint, + endHeight: uint + } +) + +;; getter for active core contract +(define-read-only (get-active-core-contract) + (begin + (asserts! (not (is-eq (var-get activeCoreContract) CONTRACT_OWNER)) ERR_NO_ACTIVE_CORE_CONTRACT) + (ok (var-get activeCoreContract)) + ) +) + +;; getter for core contract map +(define-read-only (get-core-contract-info (targetContract principal)) + (let + ( + (coreContract (unwrap! (map-get? CoreContracts targetContract) ERR_CORE_CONTRACT_NOT_FOUND)) + ) + (ok coreContract) + ) +) + +;; one-time function to initialize contracts after all contracts are deployed +;; - check that deployer is calling this function +;; - check this contract is not activated already (one-time use) +;; - set initial map value for core contract v1 +;; - set cityWallet in core contract +;; - set intialized true +(define-public (initialize-contracts (coreContract )) + (let + ( + (coreContractAddress (contract-of coreContract)) + ) + (asserts! (is-eq contract-caller CONTRACT_OWNER) ERR_UNAUTHORIZED) + (asserts! (not (var-get initialized)) ERR_UNAUTHORIZED) + (map-set CoreContracts + coreContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (try! (contract-call? coreContract set-city-wallet (var-get cityWallet))) + (var-set initialized true) + (ok true) + ) +) + +(define-read-only (is-initialized) + (var-get initialized) +) + +;; function to activate core contract through registration +;; - check that target is in core contract map +;; - check that caller is core contract +;; - check that target is in STATE_DEPLOYED +;; - set active in core contract map +;; - set as activeCoreContract +(define-public (activate-core-contract (targetContract principal) (stacksHeight uint)) + (let + ( + (coreContract (unwrap! (map-get? CoreContracts targetContract) ERR_CORE_CONTRACT_NOT_FOUND)) + ) + (asserts! (is-eq (get state coreContract) STATE_DEPLOYED) ERR_INCORRECT_CONTRACT_STATE) + (asserts! (is-eq contract-caller targetContract) ERR_UNAUTHORIZED) + (map-set CoreContracts + targetContract + { + state: STATE_ACTIVE, + startHeight: stacksHeight, + endHeight: u0 + }) + (var-set activeCoreContract targetContract) + (ok true) + ) +) + +;; protected function to update core contract +(define-public (upgrade-core-contract (oldContract ) (newContract )) + (let + ( + (oldContractAddress (contract-of oldContract)) + (oldContractMap (unwrap! (map-get? CoreContracts oldContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + (newContractAddress (contract-of newContract)) + ) + (asserts! (not (is-eq oldContractAddress newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (is-none (map-get? CoreContracts newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + (map-set CoreContracts + oldContractAddress + { + state: STATE_INACTIVE, + startHeight: (get startHeight oldContractMap), + endHeight: block-height + }) + (map-set CoreContracts + newContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (var-set activeCoreContract newContractAddress) + (try! (contract-call? oldContract shutdown-contract block-height)) + (try! (contract-call? newContract set-city-wallet (var-get cityWallet))) + (ok true) + ) +) + +(define-public (execute-upgrade-core-contract-job (jobId uint) (oldContract ) (newContract )) + (let + ( + (oldContractArg (unwrap! (get-principal-value-by-name jobId "oldContract") ERR_UNKNOWN_ARGUMENT)) + (newContractArg (unwrap! (get-principal-value-by-name jobId "newContract") ERR_UNKNOWN_ARGUMENT)) + (oldContractAddress (contract-of oldContract)) + (oldContractMap (unwrap! (map-get? CoreContracts oldContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + (newContractAddress (contract-of newContract)) + ) + (asserts! (not (is-eq oldContractAddress newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (is-none (map-get? CoreContracts newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (and (is-eq oldContractArg oldContractAddress) (is-eq newContractArg newContractAddress)) ERR_UNAUTHORIZED) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (map-set CoreContracts + oldContractAddress + { + state: STATE_INACTIVE, + startHeight: (get startHeight oldContractMap), + endHeight: block-height + }) + (map-set CoreContracts + newContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (var-set activeCoreContract newContractAddress) + (try! (contract-call? oldContract shutdown-contract block-height)) + (try! (contract-call? newContract set-city-wallet (var-get cityWallet))) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; CITY WALLET MANAGEMENT + +;; initial value for city wallet +(define-data-var cityWallet principal 'ST3PM583Q21NF0GB428P79VFPYH8X5DQVKDDGD74T) + +;; returns city wallet principal +(define-read-only (get-city-wallet) + (ok (var-get cityWallet)) +) + +;; protected function to update city wallet variable +(define-public (set-city-wallet (targetContract ) (newCityWallet principal)) + (let + ( + (coreContractAddress (contract-of targetContract)) + (coreContract (unwrap! (map-get? CoreContracts coreContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + ) + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + (asserts! (is-eq coreContractAddress (var-get activeCoreContract)) ERR_UNAUTHORIZED) + (var-set cityWallet newCityWallet) + (try! (contract-call? targetContract set-city-wallet newCityWallet)) + (ok true) + ) +) + +(define-public (execute-set-city-wallet-job (jobId uint) (targetContract )) + (let + ( + (coreContractAddress (contract-of targetContract)) + (coreContract (unwrap! (map-get? CoreContracts coreContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + (newCityWallet (unwrap! (get-principal-value-by-name jobId "newCityWallet") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (asserts! (is-eq coreContractAddress (var-get activeCoreContract)) ERR_UNAUTHORIZED) + (var-set cityWallet newCityWallet) + (try! (contract-call? targetContract set-city-wallet newCityWallet)) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; check if contract caller is city wallet +(define-private (is-authorized-city) + (is-eq contract-caller (var-get cityWallet)) +) + +;; TOKEN MANAGEMENT + +(define-public (set-token-uri (targetContract ) (newUri (optional (string-utf8 256)))) + (begin + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + (as-contract (try! (contract-call? targetContract set-token-uri newUri))) + (ok true) + ) +) + +;; COINBASE THRESHOLDS + +(define-public (update-coinbase-thresholds (targetCore ) (targetToken ) (threshold1 uint) (threshold2 uint) (threshold3 uint) (threshold4 uint) (threshold5 uint)) + (begin + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + ;; update in token contract + (as-contract (try! (contract-call? targetToken update-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5))) + ;; update core contract based on token contract + (as-contract (try! (contract-call? targetCore update-coinbase-thresholds))) + (ok true) + ) +) + +(define-public (execute-update-coinbase-thresholds-job (jobId uint) (targetCore ) (targetToken )) + (let + ( + (threshold1 (unwrap! (get-uint-value-by-name jobId "threshold1") ERR_UNKNOWN_ARGUMENT)) + (threshold2 (unwrap! (get-uint-value-by-name jobId "threshold2") ERR_UNKNOWN_ARGUMENT)) + (threshold3 (unwrap! (get-uint-value-by-name jobId "threshold3") ERR_UNKNOWN_ARGUMENT)) + (threshold4 (unwrap! (get-uint-value-by-name jobId "threshold4") ERR_UNKNOWN_ARGUMENT)) + (threshold5 (unwrap! (get-uint-value-by-name jobId "threshold5") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (as-contract (try! (contract-call? targetToken update-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5))) + (as-contract (try! (contract-call? targetCore update-coinbase-thresholds))) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; COINBASE AMOUNTS (REWARDS) + +(define-public (update-coinbase-amounts (targetCore ) (targetToken ) (amountBonus uint) (amount1 uint) (amount2 uint) (amount3 uint) (amount4 uint) (amount5 uint) (amountDefault uint)) + (begin + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + ;; update in token contract + (as-contract (try! (contract-call? targetToken update-coinbase-amounts amountBonus amount1 amount2 amount3 amount4 amount5 amountDefault))) + ;; update core contract based on token contract + (as-contract (try! (contract-call? targetCore update-coinbase-amounts))) + (ok true) + ) +) + +(define-public (execute-update-coinbase-amounts-job (jobId uint) (targetCore ) (targetToken )) + (let + ( + (amountBonus (unwrap! (get-uint-value-by-name jobId "amountBonus") ERR_UNKNOWN_ARGUMENT)) + (amount1 (unwrap! (get-uint-value-by-name jobId "amount1") ERR_UNKNOWN_ARGUMENT)) + (amount2 (unwrap! (get-uint-value-by-name jobId "amount2") ERR_UNKNOWN_ARGUMENT)) + (amount3 (unwrap! (get-uint-value-by-name jobId "amount3") ERR_UNKNOWN_ARGUMENT)) + (amount4 (unwrap! (get-uint-value-by-name jobId "amount4") ERR_UNKNOWN_ARGUMENT)) + (amount5 (unwrap! (get-uint-value-by-name jobId "amount5") ERR_UNKNOWN_ARGUMENT)) + (amountDefault (unwrap! (get-uint-value-by-name jobId "amountDefault") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (as-contract (try! (contract-call? targetToken update-coinbase-amounts amountBonus amount1 amount2 amount3 amount4 amount5 amountDefault))) + (as-contract (try! (contract-call? targetCore update-coinbase-amounts))) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; APPROVERS MANAGEMENT + +(define-public (execute-replace-approver-job (jobId uint)) + (let + ( + (oldApprover (unwrap! (get-principal-value-by-name jobId "oldApprover") ERR_UNKNOWN_ARGUMENT)) + (newApprover (unwrap! (get-principal-value-by-name jobId "newApprover") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (map-set Approvers oldApprover false) + (map-set Approvers newApprover true) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; CONTRACT INITIALIZATION + +(map-insert Approvers 'ST3AY0CM7SD9183QZ4Y7S2RGBZX9GQT54MJ6XY0BN true) +(map-insert Approvers 'ST2D06VFWWTNCWHVB2FJ9KJ3EB30HFRTHB1A4BSP3 true) +(map-insert Approvers 'ST113N3MMPZRMJJRZH6JTHA5CB7TBZH1EH4C22GFV true) +(map-insert Approvers 'ST8YRW1THF2XT8E45XXCGYKZH2B70HYH71VC7737 true) +(map-insert Approvers 'STX13Q7ZJDSFVDZMQ1PWDFGT4QSBMASRMCYE4NAP true) diff --git a/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-auth-v2.json b/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-auth-v2.json new file mode 100644 index 0000000..89c8a70 --- /dev/null +++ b/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-auth-v2.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-core-v2.clar b/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-core-v2.clar new file mode 100644 index 0000000..cd5403d --- /dev/null +++ b/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-core-v2.clar @@ -0,0 +1,1007 @@ +;; MIAMICOIN CORE CONTRACT V2 TESTNET +;; CityCoins Protocol Version 2.0.0 + +;; GENERAL CONFIGURATION + +(impl-trait 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-core-v2-trait.citycoin-core-v2) +(define-constant CONTRACT_OWNER tx-sender) + +;; ERROR CODES + +(define-constant ERR_UNAUTHORIZED (err u1000)) +(define-constant ERR_USER_ALREADY_REGISTERED (err u1001)) +(define-constant ERR_USER_NOT_FOUND (err u1002)) +(define-constant ERR_USER_ID_NOT_FOUND (err u1003)) +(define-constant ERR_ACTIVATION_THRESHOLD_REACHED (err u1004)) +(define-constant ERR_CONTRACT_NOT_ACTIVATED (err u1005)) +(define-constant ERR_USER_ALREADY_MINED (err u1006)) +(define-constant ERR_INSUFFICIENT_COMMITMENT (err u1007)) +(define-constant ERR_INSUFFICIENT_BALANCE (err u1008)) +(define-constant ERR_USER_DID_NOT_MINE_IN_BLOCK (err u1009)) +(define-constant ERR_CLAIMED_BEFORE_MATURITY (err u1010)) +(define-constant ERR_NO_MINERS_AT_BLOCK (err u1011)) +(define-constant ERR_REWARD_ALREADY_CLAIMED (err u1012)) +(define-constant ERR_MINER_DID_NOT_WIN (err u1013)) +(define-constant ERR_NO_VRF_SEED_FOUND (err u1014)) +(define-constant ERR_STACKING_NOT_AVAILABLE (err u1015)) +(define-constant ERR_CANNOT_STACK (err u1016)) +(define-constant ERR_REWARD_CYCLE_NOT_COMPLETED (err u1017)) +(define-constant ERR_NOTHING_TO_REDEEM (err u1018)) +(define-constant ERR_UNABLE_TO_FIND_CITY_WALLET (err u1019)) +(define-constant ERR_CLAIM_IN_WRONG_CONTRACT (err u1020)) +(define-constant ERR_BLOCK_HEIGHT_IN_PAST (err u1021)) +(define-constant ERR_COINBASE_AMOUNTS_NOT_FOUND (err u1022)) + +;; CITY WALLET MANAGEMENT + +;; initial value for city wallet, set to this contract until initialized +(define-data-var cityWallet principal 'ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-core-v2) + +;; returns set city wallet principal +(define-read-only (get-city-wallet) + (var-get cityWallet) +) + +;; protected function to update city wallet variable +(define-public (set-city-wallet (newCityWallet principal)) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (ok (var-set cityWallet newCityWallet)) + ) +) + +;; REGISTRATION + +(define-constant MIAMICOIN_ACTIVATION_HEIGHT u24497) +(define-data-var activationBlock uint u340282366920938463463374607431768211455) +(define-data-var activationDelay uint u1) ;; TESTNET: set to 1 block +(define-data-var activationReached bool false) +(define-data-var activationTarget uint u0) +(define-data-var activationThreshold uint u2) ;; TESTNET: set to 2 users +(define-data-var usersNonce uint u0) + +;; returns Stacks block height registration was activated at plus activationDelay +(define-read-only (get-activation-block) + (begin + (asserts! (get-activation-status) ERR_CONTRACT_NOT_ACTIVATED) + (ok (var-get activationBlock)) + ) +) + +;; returns activation delay +(define-read-only (get-activation-delay) + (var-get activationDelay) +) + +;; returns activation status as boolean +(define-read-only (get-activation-status) + (var-get activationReached) +) + +;; returns activation target +(define-read-only (get-activation-target) + (begin + (asserts! (get-activation-status) ERR_CONTRACT_NOT_ACTIVATED) + (ok (var-get activationTarget)) + ) +) + +;; returns activation threshold +(define-read-only (get-activation-threshold) + (var-get activationThreshold) +) + +;; returns number of registered users, used for activation and tracking user IDs +(define-read-only (get-registered-users-nonce) + (var-get usersNonce) +) + +;; store user principal by user id +(define-map Users + uint + principal +) + +;; store user id by user principal +(define-map UserIds + principal + uint +) + +;; returns (some userId) or none +(define-read-only (get-user-id (user principal)) + (map-get? UserIds user) +) + +;; returns (some userPrincipal) or none +(define-read-only (get-user (userId uint)) + (map-get? Users userId) +) + +;; returns user ID if it has been created, or creates and returns new ID +(define-private (get-or-create-user-id (user principal)) + (match + (map-get? UserIds user) + value value + (let + ( + (newId (+ u1 (var-get usersNonce))) + ) + (map-set Users newId user) + (map-set UserIds user newId) + (var-set usersNonce newId) + newId + ) + ) +) + +;; registers users that signal activation of contract until threshold is met +(define-public (register-user (memo (optional (string-utf8 50)))) + (let + ( + (newId (+ u1 (var-get usersNonce))) + (threshold (var-get activationThreshold)) + (initialized (contract-call? 'ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-auth-v2 is-initialized)) + ) + + (asserts! initialized ERR_UNAUTHORIZED) + + (asserts! (is-none (map-get? UserIds tx-sender)) + ERR_USER_ALREADY_REGISTERED) + + (asserts! (<= newId threshold) + ERR_ACTIVATION_THRESHOLD_REACHED) + + (if (is-some memo) + (print memo) + none + ) + + (get-or-create-user-id tx-sender) + + (if (is-eq newId threshold) + (let + ( + (activationTargetBlock (+ block-height (var-get activationDelay))) + ) + (try! (contract-call? 'ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-auth-v2 activate-core-contract (as-contract tx-sender) activationTargetBlock)) + (try! (contract-call? 'ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-token-v2 activate-token (as-contract tx-sender) MIAMICOIN_ACTIVATION_HEIGHT)) + (try! (set-coinbase-thresholds)) + (try! (set-coinbase-amounts)) + (var-set activationReached true) + (var-set activationBlock MIAMICOIN_ACTIVATION_HEIGHT) + (var-set activationTarget activationTargetBlock) + (ok true) + ) + (ok true) + ) + ) +) + +;; MINING CONFIGURATION + +;; define split to custodied wallet address for the city +(define-constant SPLIT_CITY_PCT u30) + +;; how long a miner must wait before block winner can claim their minted tokens +(define-data-var tokenRewardMaturity uint u10) ;; TESTNET: set to 10 blocks + +;; At a given Stacks block height: +;; - how many miners were there +;; - what was the total amount submitted +;; - what was the total amount submitted to the city +;; - what was the total amount submitted to Stackers +;; - was the block reward claimed +(define-map MiningStatsAtBlock + uint + { + minersCount: uint, + amount: uint, + amountToCity: uint, + amountToStackers: uint, + rewardClaimed: bool + } +) + +;; returns map MiningStatsAtBlock at a given Stacks block height if it exists +(define-read-only (get-mining-stats-at-block (stacksHeight uint)) + (map-get? MiningStatsAtBlock stacksHeight) +) + +;; returns map MiningStatsAtBlock at a given Stacks block height +;; or, an empty structure +(define-read-only (get-mining-stats-at-block-or-default (stacksHeight uint)) + (default-to { + minersCount: u0, + amount: u0, + amountToCity: u0, + amountToStackers: u0, + rewardClaimed: false + } + (map-get? MiningStatsAtBlock stacksHeight) + ) +) + +;; At a given Stacks block height and user ID: +;; - what is their ustx commitment +;; - what are the low/high values (used for VRF) +(define-map MinersAtBlock + { + stacksHeight: uint, + userId: uint + } + { + ustx: uint, + lowValue: uint, + highValue: uint, + winner: bool + } +) + +;; returns true if a given miner has already mined at a given block height +(define-read-only (has-mined-at-block (stacksHeight uint) (userId uint)) + (is-some + (map-get? MinersAtBlock { stacksHeight: stacksHeight, userId: userId }) + ) +) + +;; returns map MinersAtBlock at a given Stacks block height for a user ID +(define-read-only (get-miner-at-block (stacksHeight uint) (userId uint)) + (map-get? MinersAtBlock { stacksHeight: stacksHeight, userId: userId }) +) + +;; returns map MinersAtBlock at a given Stacks block height for a user ID +;; or, an empty structure +(define-read-only (get-miner-at-block-or-default (stacksHeight uint) (userId uint)) + (default-to { + highValue: u0, + lowValue: u0, + ustx: u0, + winner: false + } + (map-get? MinersAtBlock { stacksHeight: stacksHeight, userId: userId })) +) + +;; At a given Stacks block height: +;; - what is the max highValue from MinersAtBlock (used for VRF) +(define-map MinersAtBlockHighValue + uint + uint +) + +;; returns last high value from map MinersAtBlockHighValue +(define-read-only (get-last-high-value-at-block (stacksHeight uint)) + (default-to u0 + (map-get? MinersAtBlockHighValue stacksHeight)) +) + +;; At a given Stacks block height: +;; - what is the userId of miner who won this block +(define-map BlockWinnerIds + uint + uint +) + +(define-read-only (get-block-winner-id (stacksHeight uint)) + (map-get? BlockWinnerIds stacksHeight) +) + +;; MINING ACTIONS + +(define-public (mine-tokens (amountUstx uint) (memo (optional (buff 34)))) + (let + ( + (userId (get-or-create-user-id tx-sender)) + ) + (try! (mine-tokens-at-block userId block-height amountUstx memo)) + (ok true) + ) +) + +(define-public (mine-many (amounts (list 200 uint))) + (begin + (asserts! (is-activated) ERR_CONTRACT_NOT_ACTIVATED) + (asserts! (> (len amounts) u0) ERR_INSUFFICIENT_COMMITMENT) + (match (fold mine-single amounts (ok { userId: (get-or-create-user-id tx-sender), toStackers: u0, toCity: u0, stacksHeight: block-height })) + okReturn + (begin + (asserts! (>= (stx-get-balance tx-sender) (+ (get toStackers okReturn) (get toCity okReturn))) ERR_INSUFFICIENT_BALANCE) + (if (> (get toStackers okReturn ) u0) + (try! (stx-transfer? (get toStackers okReturn ) tx-sender (as-contract tx-sender))) + false + ) + (try! (stx-transfer? (get toCity okReturn) tx-sender (var-get cityWallet))) + (print { + firstBlock: block-height, + lastBlock: (- (+ block-height (len amounts)) u1) + }) + (ok true) + ) + errReturn (err errReturn) + ) + ) +) + +(define-private (mine-single + (amountUstx uint) + (return (response + { + userId: uint, + toStackers: uint, + toCity: uint, + stacksHeight: uint + } + uint + ))) + + (match return okReturn + (let + ( + (stacksHeight (get stacksHeight okReturn)) + (rewardCycle (default-to u0 (get-reward-cycle stacksHeight))) + (stackingActive (stacking-active-at-cycle rewardCycle)) + (toCity + (if stackingActive + (/ (* SPLIT_CITY_PCT amountUstx) u100) + amountUstx + ) + ) + (toStackers (- amountUstx toCity)) + ) + (asserts! (not (has-mined-at-block stacksHeight (get userId okReturn))) ERR_USER_ALREADY_MINED) + (asserts! (> amountUstx u0) ERR_INSUFFICIENT_COMMITMENT) + (try! (set-tokens-mined (get userId okReturn) stacksHeight amountUstx toStackers toCity)) + (ok (merge okReturn + { + toStackers: (+ (get toStackers okReturn) toStackers), + toCity: (+ (get toCity okReturn) toCity), + stacksHeight: (+ stacksHeight u1) + } + )) + ) + errReturn (err errReturn) + ) +) + +(define-private (mine-tokens-at-block (userId uint) (stacksHeight uint) (amountUstx uint) (memo (optional (buff 34)))) + (let + ( + (rewardCycle (default-to u0 (get-reward-cycle stacksHeight))) + (stackingActive (stacking-active-at-cycle rewardCycle)) + (toCity + (if stackingActive + (/ (* SPLIT_CITY_PCT amountUstx) u100) + amountUstx + ) + ) + (toStackers (- amountUstx toCity)) + ) + (asserts! (is-activated) ERR_CONTRACT_NOT_ACTIVATED) + (asserts! (not (has-mined-at-block stacksHeight userId)) ERR_USER_ALREADY_MINED) + (asserts! (> amountUstx u0) ERR_INSUFFICIENT_COMMITMENT) + (asserts! (>= (stx-get-balance tx-sender) amountUstx) ERR_INSUFFICIENT_BALANCE) + (try! (set-tokens-mined userId stacksHeight amountUstx toStackers toCity)) + (if (is-some memo) + (print memo) + none + ) + (if stackingActive + (try! (stx-transfer? toStackers tx-sender (as-contract tx-sender))) + false + ) + (try! (stx-transfer? toCity tx-sender (var-get cityWallet))) + (ok true) + ) +) + +(define-private (set-tokens-mined (userId uint) (stacksHeight uint) (amountUstx uint) (toStackers uint) (toCity uint)) + (let + ( + (blockStats (get-mining-stats-at-block-or-default stacksHeight)) + (newMinersCount (+ (get minersCount blockStats) u1)) + (minerLowVal (get-last-high-value-at-block stacksHeight)) + (rewardCycle (unwrap! (get-reward-cycle stacksHeight) + ERR_STACKING_NOT_AVAILABLE)) + (rewardCycleStats (get-stacking-stats-at-cycle-or-default rewardCycle)) + ) + (map-set MiningStatsAtBlock + stacksHeight + { + minersCount: newMinersCount, + amount: (+ (get amount blockStats) amountUstx), + amountToCity: (+ (get amountToCity blockStats) toCity), + amountToStackers: (+ (get amountToStackers blockStats) toStackers), + rewardClaimed: false + } + ) + (map-set MinersAtBlock + { + stacksHeight: stacksHeight, + userId: userId + } + { + ustx: amountUstx, + lowValue: (if (> minerLowVal u0) (+ minerLowVal u1) u0), + highValue: (+ minerLowVal amountUstx), + winner: false + } + ) + (map-set MinersAtBlockHighValue + stacksHeight + (+ minerLowVal amountUstx) + ) + (if (> toStackers u0) + (map-set StackingStatsAtCycle + rewardCycle + { + amountUstx: (+ (get amountUstx rewardCycleStats) toStackers), + amountToken: (get amountToken rewardCycleStats) + } + ) + false + ) + (ok true) + ) +) + +;; MINING REWARD CLAIM ACTIONS + +;; calls function to claim mining reward in active logic contract +(define-public (claim-mining-reward (minerBlockHeight uint)) + (begin + (asserts! (or (is-eq (var-get shutdownHeight) u0) (< minerBlockHeight (var-get shutdownHeight))) ERR_CLAIM_IN_WRONG_CONTRACT) + (try! (claim-mining-reward-at-block tx-sender block-height minerBlockHeight)) + (ok true) + ) +) + +;; Determine whether or not the given principal can claim the mined tokens at a particular block height, +;; given the miners record for that block height, a random sample, and the current block height. +(define-private (claim-mining-reward-at-block (user principal) (stacksHeight uint) (minerBlockHeight uint)) + (let + ( + (maturityHeight (+ (var-get tokenRewardMaturity) minerBlockHeight)) + (userId (unwrap! (get-user-id user) ERR_USER_ID_NOT_FOUND)) + (blockStats (unwrap! (get-mining-stats-at-block minerBlockHeight) ERR_NO_MINERS_AT_BLOCK)) + (minerStats (unwrap! (get-miner-at-block minerBlockHeight userId) ERR_USER_DID_NOT_MINE_IN_BLOCK)) + (isMature (asserts! (> stacksHeight maturityHeight) ERR_CLAIMED_BEFORE_MATURITY)) + (vrfSample (unwrap! (contract-call? 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-vrf-v2 get-save-rnd maturityHeight) ERR_NO_VRF_SEED_FOUND)) + (commitTotal (get-last-high-value-at-block minerBlockHeight)) + (winningValue (mod vrfSample commitTotal)) + ) + (asserts! (not (get rewardClaimed blockStats)) ERR_REWARD_ALREADY_CLAIMED) + (asserts! (and (>= winningValue (get lowValue minerStats)) (<= winningValue (get highValue minerStats))) + ERR_MINER_DID_NOT_WIN) + (try! (set-mining-reward-claimed userId minerBlockHeight)) + (ok true) + ) +) + +(define-private (set-mining-reward-claimed (userId uint) (minerBlockHeight uint)) + (let + ( + (blockStats (get-mining-stats-at-block-or-default minerBlockHeight)) + (minerStats (get-miner-at-block-or-default minerBlockHeight userId)) + (user (unwrap! (get-user userId) ERR_USER_NOT_FOUND)) + ) + (map-set MiningStatsAtBlock + minerBlockHeight + { + minersCount: (get minersCount blockStats), + amount: (get amount blockStats), + amountToCity: (get amountToCity blockStats), + amountToStackers: (get amountToStackers blockStats), + rewardClaimed: true + } + ) + (map-set MinersAtBlock + { + stacksHeight: minerBlockHeight, + userId: userId + } + { + ustx: (get ustx minerStats), + lowValue: (get lowValue minerStats), + highValue: (get highValue minerStats), + winner: true + } + ) + (map-set BlockWinnerIds + minerBlockHeight + userId + ) + (try! (mint-coinbase user minerBlockHeight)) + (ok true) + ) +) + +(define-read-only (is-block-winner (user principal) (minerBlockHeight uint)) + (is-block-winner-and-can-claim user minerBlockHeight false) +) + +(define-read-only (can-claim-mining-reward (user principal) (minerBlockHeight uint)) + (is-block-winner-and-can-claim user minerBlockHeight true) +) + +(define-private (is-block-winner-and-can-claim (user principal) (minerBlockHeight uint) (testCanClaim bool)) + (let + ( + (userId (unwrap! (get-user-id user) false)) + (blockStats (unwrap! (get-mining-stats-at-block minerBlockHeight) false)) + (minerStats (unwrap! (get-miner-at-block minerBlockHeight userId) false)) + (maturityHeight (+ (var-get tokenRewardMaturity) minerBlockHeight)) + (vrfSample (unwrap! (contract-call? 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-vrf-v2 get-rnd maturityHeight) false)) + (commitTotal (get-last-high-value-at-block minerBlockHeight)) + (winningValue (mod vrfSample commitTotal)) + ) + (if (and (>= winningValue (get lowValue minerStats)) (<= winningValue (get highValue minerStats))) + (if testCanClaim (not (get rewardClaimed blockStats)) true) + false + ) + ) +) + +;; STACKING CONFIGURATION + +(define-constant MAX_REWARD_CYCLES u32) +(define-constant REWARD_CYCLE_INDEXES (list u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11 u12 u13 u14 u15 u16 u17 u18 u19 u20 u21 u22 u23 u24 u25 u26 u27 u28 u29 u30 u31)) + +;; how long a reward cycle is +(define-data-var rewardCycleLength uint u20) ;; TESTNET: set to 20 blocks + +;; At a given reward cycle: +;; - how many Stackers were there +;; - what is the total uSTX submitted by miners +;; - what is the total amount of tokens stacked +(define-map StackingStatsAtCycle + uint + { + amountUstx: uint, + amountToken: uint + } +) + +;; returns the total stacked tokens and committed uSTX for a given reward cycle +(define-read-only (get-stacking-stats-at-cycle (rewardCycle uint)) + (map-get? StackingStatsAtCycle rewardCycle) +) + +;; returns the total stacked tokens and committed uSTX for a given reward cycle +;; or, an empty structure +(define-read-only (get-stacking-stats-at-cycle-or-default (rewardCycle uint)) + (default-to { amountUstx: u0, amountToken: u0 } + (map-get? StackingStatsAtCycle rewardCycle)) +) + +;; At a given reward cycle and user ID: +;; - what is the total tokens Stacked? +;; - how many tokens should be returned? (based on Stacking period) +(define-map StackerAtCycle + { + rewardCycle: uint, + userId: uint + } + { + amountStacked: uint, + toReturn: uint + } +) + +(define-read-only (get-stacker-at-cycle (rewardCycle uint) (userId uint)) + (map-get? StackerAtCycle { rewardCycle: rewardCycle, userId: userId }) +) + +(define-read-only (get-stacker-at-cycle-or-default (rewardCycle uint) (userId uint)) + (default-to { amountStacked: u0, toReturn: u0 } + (map-get? StackerAtCycle { rewardCycle: rewardCycle, userId: userId })) +) + +;; get the reward cycle for a given Stacks block height +(define-read-only (get-reward-cycle (stacksHeight uint)) + (let + ( + (firstStackingBlock (var-get activationBlock)) + (rcLen (var-get rewardCycleLength)) + ) + (if (>= stacksHeight firstStackingBlock) + (some (/ (- stacksHeight firstStackingBlock) rcLen)) + none) + ) +) + +;; determine if stacking is active in a given cycle +(define-read-only (stacking-active-at-cycle (rewardCycle uint)) + (is-some + (get amountToken (map-get? StackingStatsAtCycle rewardCycle)) + ) +) + +;; get the first Stacks block height for a given reward cycle. +(define-read-only (get-first-stacks-block-in-reward-cycle (rewardCycle uint)) + (+ (var-get activationBlock) (* (var-get rewardCycleLength) rewardCycle)) +) + +;; getter for get-entitled-stacking-reward that specifies block height +(define-read-only (get-stacking-reward (userId uint) (targetCycle uint)) + (get-entitled-stacking-reward userId targetCycle block-height) +) + +;; get uSTX a Stacker can claim, given reward cycle they stacked in and current block height +;; this method only returns a positive value if: +;; - the current block height is in a subsequent reward cycle +;; - the stacker actually locked up tokens in the target reward cycle +;; - the stacker locked up _enough_ tokens to get at least one uSTX +;; it is possible to Stack tokens and not receive uSTX: +;; - if no miners commit during this reward cycle +;; - the amount stacked by user is too few that you'd be entitled to less than 1 uSTX +(define-private (get-entitled-stacking-reward (userId uint) (targetCycle uint) (stacksHeight uint)) + (let + ( + (rewardCycleStats (get-stacking-stats-at-cycle-or-default targetCycle)) + (stackerAtCycle (get-stacker-at-cycle-or-default targetCycle userId)) + (totalUstxThisCycle (get amountUstx rewardCycleStats)) + (totalStackedThisCycle (get amountToken rewardCycleStats)) + (userStackedThisCycle (get amountStacked stackerAtCycle)) + ) + (match (get-reward-cycle stacksHeight) + currentCycle + (if (and (not (var-get isShutdown)) + (or (<= currentCycle targetCycle) (is-eq u0 userStackedThisCycle))) + ;; the contract is not shut down and + ;; this cycle hasn't finished + ;; or stacker contributed nothing + u0 + ;; (totalUstxThisCycle * userStackedThisCycle) / totalStackedThisCycle + (/ (* totalUstxThisCycle userStackedThisCycle) totalStackedThisCycle) + ) + ;; before first reward cycle + u0 + ) + ) +) + +;; STACKING ACTIONS + +(define-public (stack-tokens (amountTokens uint) (lockPeriod uint)) + (let + ( + (userId (get-or-create-user-id tx-sender)) + ) + (try! (stack-tokens-at-cycle tx-sender userId amountTokens block-height lockPeriod)) + (ok true) + ) +) + +(define-private (stack-tokens-at-cycle (user principal) (userId uint) (amountTokens uint) (startHeight uint) (lockPeriod uint)) + (let + ( + (currentCycle (unwrap! (get-reward-cycle startHeight) ERR_STACKING_NOT_AVAILABLE)) + (targetCycle (+ u1 currentCycle)) + (commitment { + stackerId: userId, + amount: amountTokens, + first: targetCycle, + last: (+ targetCycle lockPeriod) + }) + ) + (asserts! (is-activated) ERR_CONTRACT_NOT_ACTIVATED) + (asserts! (and (> lockPeriod u0) (<= lockPeriod MAX_REWARD_CYCLES)) + ERR_CANNOT_STACK) + (asserts! (> amountTokens u0) ERR_CANNOT_STACK) + (try! (contract-call? 'ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-token-v2 transfer amountTokens tx-sender (as-contract tx-sender) none)) + (print { + firstCycle: targetCycle, + lastCycle: (- (+ targetCycle lockPeriod) u1) + }) + (match (fold stack-tokens-closure REWARD_CYCLE_INDEXES (ok commitment)) + okValue (ok true) + errValue (err errValue) + ) + ) +) + +(define-private (stack-tokens-closure (rewardCycleIdx uint) + (commitmentResponse (response + { + stackerId: uint, + amount: uint, + first: uint, + last: uint + } + uint + ))) + + (match commitmentResponse + commitment + (let + ( + (stackerId (get stackerId commitment)) + (amountToken (get amount commitment)) + (firstCycle (get first commitment)) + (lastCycle (get last commitment)) + (targetCycle (+ firstCycle rewardCycleIdx)) + ) + (begin + (if (and (>= targetCycle firstCycle) (< targetCycle lastCycle)) + (begin + (if (is-eq targetCycle (- lastCycle u1)) + (set-tokens-stacked stackerId targetCycle amountToken amountToken) + (set-tokens-stacked stackerId targetCycle amountToken u0) + ) + true + ) + false + ) + commitmentResponse + ) + ) + errValue commitmentResponse + ) +) + +(define-private (set-tokens-stacked (userId uint) (targetCycle uint) (amountStacked uint) (toReturn uint)) + (let + ( + (rewardCycleStats (get-stacking-stats-at-cycle-or-default targetCycle)) + (stackerAtCycle (get-stacker-at-cycle-or-default targetCycle userId)) + ) + (map-set StackingStatsAtCycle + targetCycle + { + amountUstx: (get amountUstx rewardCycleStats), + amountToken: (+ amountStacked (get amountToken rewardCycleStats)) + } + ) + (map-set StackerAtCycle + { + rewardCycle: targetCycle, + userId: userId + } + { + amountStacked: (+ amountStacked (get amountStacked stackerAtCycle)), + toReturn: (+ toReturn (get toReturn stackerAtCycle)) + } + ) + ) +) + +;; STACKING REWARD CLAIMS + +;; calls function to claim stacking reward in active logic contract +(define-public (claim-stacking-reward (targetCycle uint)) + (begin + (try! (claim-stacking-reward-at-cycle tx-sender block-height targetCycle)) + (ok true) + ) +) + +(define-private (claim-stacking-reward-at-cycle (user principal) (stacksHeight uint) (targetCycle uint)) + (let + ( + (currentCycle (unwrap! (get-reward-cycle stacksHeight) ERR_STACKING_NOT_AVAILABLE)) + (userId (unwrap! (get-user-id user) ERR_USER_ID_NOT_FOUND)) + (entitledUstx (get-entitled-stacking-reward userId targetCycle stacksHeight)) + (stackerAtCycle (get-stacker-at-cycle-or-default targetCycle userId)) + (toReturn (get toReturn stackerAtCycle)) + ) + (asserts! (or + (is-eq true (var-get isShutdown)) + (> currentCycle targetCycle)) + ERR_REWARD_CYCLE_NOT_COMPLETED) + (asserts! (or (> toReturn u0) (> entitledUstx u0)) ERR_NOTHING_TO_REDEEM) + ;; disable ability to claim again + (map-set StackerAtCycle + { + rewardCycle: targetCycle, + userId: userId + } + { + amountStacked: u0, + toReturn: u0 + } + ) + ;; send back tokens if user was eligible + (if (> toReturn u0) + (try! (as-contract (contract-call? 'ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-token-v2 transfer toReturn tx-sender user none))) + true + ) + ;; send back rewards if user was eligible + (if (> entitledUstx u0) + (try! (as-contract (stx-transfer? entitledUstx tx-sender user))) + true + ) + (ok true) + ) +) + +;; TOKEN CONFIGURATION + +;; decimals and multiplier for token +(define-constant DECIMALS u6) +(define-constant MICRO_CITYCOINS (pow u10 DECIMALS)) + +;; bonus period length for increased coinbase rewards +(define-constant TOKEN_BONUS_PERIOD u10000) + +;; coinbase thresholds per halving, used to determine halvings +(define-data-var coinbaseThreshold1 uint u0) +(define-data-var coinbaseThreshold2 uint u0) +(define-data-var coinbaseThreshold3 uint u0) +(define-data-var coinbaseThreshold4 uint u0) +(define-data-var coinbaseThreshold5 uint u0) + +;; return coinbase thresholds if contract activated +(define-read-only (get-coinbase-thresholds) + (let + ( + (activated (get-activation-status)) + ) + (asserts! activated ERR_CONTRACT_NOT_ACTIVATED) + (ok { + coinbaseThreshold1: (var-get coinbaseThreshold1), + coinbaseThreshold2: (var-get coinbaseThreshold2), + coinbaseThreshold3: (var-get coinbaseThreshold3), + coinbaseThreshold4: (var-get coinbaseThreshold4), + coinbaseThreshold5: (var-get coinbaseThreshold5) + }) + ) +) + +;; set coinbase thresholds, used during activation +(define-private (set-coinbase-thresholds) + (let + ( + (coinbaseThresholds (try! (contract-call? 'ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-token-v2 get-coinbase-thresholds))) + ) + (var-set coinbaseThreshold1 (get coinbaseThreshold1 coinbaseThresholds)) + (var-set coinbaseThreshold2 (get coinbaseThreshold2 coinbaseThresholds)) + (var-set coinbaseThreshold3 (get coinbaseThreshold3 coinbaseThresholds)) + (var-set coinbaseThreshold4 (get coinbaseThreshold4 coinbaseThresholds)) + (var-set coinbaseThreshold5 (get coinbaseThreshold5 coinbaseThresholds)) + ;; print coinbase thresholds + (print { + coinbaseThreshold1: (var-get coinbaseThreshold1), + coinbaseThreshold2: (var-get coinbaseThreshold2), + coinbaseThreshold3: (var-get coinbaseThreshold3), + coinbaseThreshold4: (var-get coinbaseThreshold4), + coinbaseThreshold5: (var-get coinbaseThreshold5) + }) + (ok true) + ) +) + +;; guarded function for auth to update coinbase thresholds +(define-public (update-coinbase-thresholds) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (try! (set-coinbase-thresholds)) + (ok true) + ) +) + +;; coinbase rewards per threshold, used to determine rewards +(define-data-var coinbaseAmountBonus uint u0) +(define-data-var coinbaseAmount1 uint u0) +(define-data-var coinbaseAmount2 uint u0) +(define-data-var coinbaseAmount3 uint u0) +(define-data-var coinbaseAmount4 uint u0) +(define-data-var coinbaseAmount5 uint u0) +(define-data-var coinbaseAmountDefault uint u0) + +;; return coinbase amounts if contract activated +(define-read-only (get-coinbase-amounts) + (let + ( + (activated (get-activation-status)) + ) + (asserts! activated ERR_CONTRACT_NOT_ACTIVATED) + (ok { + coinbaseAmountBonus: (var-get coinbaseAmountBonus), + coinbaseAmount1: (var-get coinbaseAmount1), + coinbaseAmount2: (var-get coinbaseAmount2), + coinbaseAmount3: (var-get coinbaseAmount3), + coinbaseAmount4: (var-get coinbaseAmount4), + coinbaseAmount5: (var-get coinbaseAmount5), + coinbaseAmountDefault: (var-get coinbaseAmountDefault) + }) + ) +) + +;; set coinbase amounts, used during activation +(define-private (set-coinbase-amounts) + (let + ( + (coinbaseAmounts (unwrap! (contract-call? 'ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-token-v2 get-coinbase-amounts) ERR_COINBASE_AMOUNTS_NOT_FOUND)) + ) + (var-set coinbaseAmountBonus (get coinbaseAmountBonus coinbaseAmounts)) + (var-set coinbaseAmount1 (get coinbaseAmount1 coinbaseAmounts)) + (var-set coinbaseAmount2 (get coinbaseAmount2 coinbaseAmounts)) + (var-set coinbaseAmount3 (get coinbaseAmount3 coinbaseAmounts)) + (var-set coinbaseAmount4 (get coinbaseAmount4 coinbaseAmounts)) + (var-set coinbaseAmount5 (get coinbaseAmount5 coinbaseAmounts)) + (var-set coinbaseAmountDefault (get coinbaseAmountDefault coinbaseAmounts)) + ;; print coinbase amounts + (print { + coinbaseAmountBonus: (var-get coinbaseAmountBonus), + coinbaseAmount1: (var-get coinbaseAmount1), + coinbaseAmount2: (var-get coinbaseAmount2), + coinbaseAmount3: (var-get coinbaseAmount3), + coinbaseAmount4: (var-get coinbaseAmount4), + coinbaseAmount5: (var-get coinbaseAmount5), + coinbaseAmountDefault: (var-get coinbaseAmountDefault) + }) + (ok true) + ) +) + +;; guarded function for auth to update coinbase amounts +(define-public (update-coinbase-amounts) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (try! (set-coinbase-amounts)) + (ok true) + ) +) + +;; function for deciding how many tokens to mint, depending on when they were mined +(define-read-only (get-coinbase-amount (minerBlockHeight uint)) + (begin + ;; if contract is not active, return 0 + (asserts! (>= minerBlockHeight (var-get activationBlock)) u0) + ;; if contract is active, return based on emissions schedule + ;; defined in CCIP-008 https://github.com/citycoins/governance + (asserts! (> minerBlockHeight (var-get coinbaseThreshold1)) + (if (<= (- minerBlockHeight (var-get activationBlock)) TOKEN_BONUS_PERIOD) + ;; bonus reward for initial miners + (var-get coinbaseAmountBonus) + ;; standard reward until 1st halving + (var-get coinbaseAmount1) + ) + ) + ;; computations based on each halving threshold + (asserts! (> minerBlockHeight (var-get coinbaseThreshold2)) (var-get coinbaseAmount2)) + (asserts! (> minerBlockHeight (var-get coinbaseThreshold3)) (var-get coinbaseAmount3)) + (asserts! (> minerBlockHeight (var-get coinbaseThreshold4)) (var-get coinbaseAmount4)) + (asserts! (> minerBlockHeight (var-get coinbaseThreshold5)) (var-get coinbaseAmount5)) + ;; default value after 5th halving + (var-get coinbaseAmountDefault) + ) +) + +;; mint new tokens for claimant who won at given Stacks block height +(define-private (mint-coinbase (recipient principal) (stacksHeight uint)) + (as-contract (contract-call? 'ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-token-v2 mint (get-coinbase-amount stacksHeight) recipient)) +) + +;; UTILITIES + +(define-data-var shutdownHeight uint u0) +(define-data-var isShutdown bool false) + +;; stop mining and stacking operations +;; in preparation for a core upgrade +(define-public (shutdown-contract (stacksHeight uint)) + (begin + ;; make sure block height is in the future + (asserts! (>= stacksHeight block-height) ERR_BLOCK_HEIGHT_IN_PAST) + ;; only allow shutdown request from AUTH + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + ;; set variables to disable mining/stacking in CORE + (var-set activationReached false) + (var-set shutdownHeight stacksHeight) + ;; set variable to allow for all stacking claims + (var-set isShutdown true) + (ok true) + ) +) + +;; checks if caller is Auth contract +(define-private (is-authorized-auth) + (is-eq contract-caller 'ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-auth-v2) +) + +;; checks if contract is fully activated to +;; enable mining and stacking functions +(define-private (is-activated) + (and (get-activation-status) (>= block-height (var-get activationTarget))) +) diff --git a/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-core-v2.json b/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-core-v2.json new file mode 100644 index 0000000..89c8a70 --- /dev/null +++ b/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-core-v2.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-token-v2.clar b/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-token-v2.clar new file mode 100644 index 0000000..60cd561 --- /dev/null +++ b/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-token-v2.clar @@ -0,0 +1,281 @@ +;; MIAMICOIN TOKEN V2 CONTRACT TESTNET +;; CityCoins Protocol Version 2.0.0 + +;; TRAIT DEFINITIONS + +(impl-trait 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-token-v2-trait.citycoin-token-v2) + +;; ERROR CODES + +(define-constant ERR_UNAUTHORIZED (err u2000)) +(define-constant ERR_TOKEN_NOT_ACTIVATED (err u2001)) +(define-constant ERR_TOKEN_ALREADY_ACTIVATED (err u2002)) +(define-constant ERR_V1_BALANCE_NOT_FOUND (err u2003)) +(define-constant ERR_INVALID_COINBASE_THRESHOLD (err u2004)) +(define-constant ERR_INVALID_COINBASE_AMOUNT (err u2005)) + +;; SIP-010 DEFINITION + +(impl-trait 'ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.sip-010-trait-ft-standard.sip-010-trait) + +(define-fungible-token miamicoin) + +(define-constant DECIMALS u6) +(define-constant MICRO_CITYCOINS (pow u10 DECIMALS)) + +;; SIP-010 FUNCTIONS + +(define-public (transfer (amount uint) (from principal) (to principal) (memo (optional (buff 34)))) + (begin + (asserts! (is-eq from tx-sender) ERR_UNAUTHORIZED) + (if (is-some memo) + (print memo) + none + ) + (ft-transfer? miamicoin amount from to) + ) +) + +(define-read-only (get-name) + (ok "miamicoin") +) + +(define-read-only (get-symbol) + (ok "MIA") +) + +(define-read-only (get-decimals) + (ok DECIMALS) +) + +(define-read-only (get-balance (user principal)) + (ok (ft-get-balance miamicoin user)) +) + +(define-read-only (get-total-supply) + (ok (ft-get-supply miamicoin)) +) + +(define-read-only (get-token-uri) + (ok (var-get tokenUri)) +) + +;; TOKEN CONFIGURATION + +;; define bonus period and initial epoch length +(define-constant TOKEN_BONUS_PERIOD u10000) +(define-constant TOKEN_EPOCH_LENGTH u25000) + +;; once activated, activation cannot happen again +(define-data-var tokenActivated bool false) + +;; core contract states +(define-constant STATE_DEPLOYED u0) +(define-constant STATE_ACTIVE u1) +(define-constant STATE_INACTIVE u2) + +;; one-time function to activate the token +(define-public (activate-token (coreContract principal) (stacksHeight uint)) + (let + ( + (coreContractMap (try! (contract-call? 'ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-auth-v2 get-core-contract-info coreContract))) + (threshold1 (+ stacksHeight TOKEN_BONUS_PERIOD TOKEN_EPOCH_LENGTH)) ;; 35,000 blocks + (threshold2 (+ stacksHeight TOKEN_BONUS_PERIOD (* u3 TOKEN_EPOCH_LENGTH))) ;; 85,000 blocks + (threshold3 (+ stacksHeight TOKEN_BONUS_PERIOD (* u7 TOKEN_EPOCH_LENGTH))) ;; 185,000 blocks + (threshold4 (+ stacksHeight TOKEN_BONUS_PERIOD (* u15 TOKEN_EPOCH_LENGTH))) ;; 385,000 blocks + (threshold5 (+ stacksHeight TOKEN_BONUS_PERIOD (* u31 TOKEN_EPOCH_LENGTH))) ;; 785,000 blocks + ) + (asserts! (is-eq (get state coreContractMap) STATE_ACTIVE) ERR_UNAUTHORIZED) + (asserts! (not (var-get tokenActivated)) ERR_TOKEN_ALREADY_ACTIVATED) + (var-set tokenActivated true) + (try! (set-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5)) + (ok true) + ) +) + +;; COINBASE THRESHOLDS + +;; coinbase thresholds per halving, used to select coinbase rewards in core +;; initially set by register-user in core contract per CCIP-008 +(define-data-var coinbaseThreshold1 uint u0) +(define-data-var coinbaseThreshold2 uint u0) +(define-data-var coinbaseThreshold3 uint u0) +(define-data-var coinbaseThreshold4 uint u0) +(define-data-var coinbaseThreshold5 uint u0) + +;; return coinbase thresholds if token activated +(define-read-only (get-coinbase-thresholds) + (let + ( + (activated (var-get tokenActivated)) + ) + (asserts! activated ERR_TOKEN_NOT_ACTIVATED) + (ok { + coinbaseThreshold1: (var-get coinbaseThreshold1), + coinbaseThreshold2: (var-get coinbaseThreshold2), + coinbaseThreshold3: (var-get coinbaseThreshold3), + coinbaseThreshold4: (var-get coinbaseThreshold4), + coinbaseThreshold5: (var-get coinbaseThreshold5) + }) + ) +) + +(define-private (set-coinbase-thresholds (threshold1 uint) (threshold2 uint) (threshold3 uint) (threshold4 uint) (threshold5 uint)) + (begin + ;; check that all thresholds increase in value + (asserts! (and (> threshold1 u0) (> threshold2 threshold1) (> threshold3 threshold2) (> threshold4 threshold3) (> threshold5 threshold4)) ERR_INVALID_COINBASE_THRESHOLD) + ;; set coinbase thresholds + (var-set coinbaseThreshold1 threshold1) + (var-set coinbaseThreshold2 threshold2) + (var-set coinbaseThreshold3 threshold3) + (var-set coinbaseThreshold4 threshold4) + (var-set coinbaseThreshold5 threshold5) + ;; print coinbase thresholds + (print { + coinbaseThreshold1: threshold1, + coinbaseThreshold2: threshold2, + coinbaseThreshold3: threshold3, + coinbaseThreshold4: threshold4, + coinbaseThreshold5: threshold5 + }) + (ok true) + ) +) + +;; only accessible by auth +(define-public (update-coinbase-thresholds (threshold1 uint) (threshold2 uint) (threshold3 uint) (threshold4 uint) (threshold5 uint)) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (asserts! (var-get tokenActivated) ERR_TOKEN_NOT_ACTIVATED) + (try! (set-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5)) + (ok true) + ) +) + +;; COINBASE AMOUNTS (REWARDS) + +;; coinbase rewards per threshold per CCIP-008 +(define-data-var coinbaseAmountBonus uint (* MICRO_CITYCOINS u250000)) +(define-data-var coinbaseAmount1 uint (* MICRO_CITYCOINS u100000)) +(define-data-var coinbaseAmount2 uint (* MICRO_CITYCOINS u50000)) +(define-data-var coinbaseAmount3 uint (* MICRO_CITYCOINS u25000)) +(define-data-var coinbaseAmount4 uint (* MICRO_CITYCOINS u12500)) +(define-data-var coinbaseAmount5 uint (* MICRO_CITYCOINS u6250)) +(define-data-var coinbaseAmountDefault uint (* MICRO_CITYCOINS u3125)) + +;; return coinbase thresholds if token activated +(define-read-only (get-coinbase-amounts) + (ok { + coinbaseAmountBonus: (var-get coinbaseAmountBonus), + coinbaseAmount1: (var-get coinbaseAmount1), + coinbaseAmount2: (var-get coinbaseAmount2), + coinbaseAmount3: (var-get coinbaseAmount3), + coinbaseAmount4: (var-get coinbaseAmount4), + coinbaseAmount5: (var-get coinbaseAmount5), + coinbaseAmountDefault: (var-get coinbaseAmountDefault) + }) +) + +(define-private (set-coinbase-amounts (amountBonus uint) (amount1 uint) (amount2 uint) (amount3 uint) (amount4 uint) (amount5 uint) (amountDefault uint)) + (begin + ;; check that all amounts are greater than zero + (asserts! (and (> amountBonus u0) (> amount1 u0) (> amount2 u0) (> amount3 u0) (> amount4 u0) (> amount5 u0) (> amountDefault u0)) ERR_INVALID_COINBASE_AMOUNT) + ;; set coinbase amounts in token contract + (var-set coinbaseAmountBonus amountBonus) + (var-set coinbaseAmount1 amount1) + (var-set coinbaseAmount2 amount2) + (var-set coinbaseAmount3 amount3) + (var-set coinbaseAmount4 amount4) + (var-set coinbaseAmount5 amount5) + (var-set coinbaseAmountDefault amountDefault) + ;; print coinbase amounts + (print { + coinbaseAmountBonus: amountBonus, + coinbaseAmount1: amount1, + coinbaseAmount2: amount2, + coinbaseAmount3: amount3, + coinbaseAmount4: amount4, + coinbaseAmount5: amount5, + coinbaseAmountDefault: amountDefault + }) + (ok true) + ) +) + +;; only accessible by auth +(define-public (update-coinbase-amounts (amountBonus uint) (amount1 uint) (amount2 uint) (amount3 uint) (amount4 uint) (amount5 uint) (amountDefault uint)) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + ;; (asserts! (var-get tokenActivated) ERR_TOKEN_NOT_ACTIVATED) + (try! (set-coinbase-amounts amountBonus amount1 amount2 amount3 amount4 amount5 amountDefault)) + (ok true) + ) +) + +;; V1 TO V2 CONVERSION + +;; TESTNET: convert-to-v2 removed, no v1 + +;; UTILITIES + +(define-data-var tokenUri (optional (string-utf8 256)) (some u"https://cdn.citycoins.co/metadata/miamicoin.json")) + +;; set token URI to new value, only accessible by Auth +(define-public (set-token-uri (newUri (optional (string-utf8 256)))) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (ok (var-set tokenUri newUri)) + ) +) + +;; mint new tokens, only accessible by a Core contract +(define-public (mint (amount uint) (recipient principal)) + (let + ( + (coreContract (try! (contract-call? 'ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-auth-v2 get-core-contract-info contract-caller))) + ) + (ft-mint? miamicoin amount recipient) + ) +) + +;; burn tokens +(define-public (burn (amount uint) (owner principal)) + (begin + (asserts! (is-eq tx-sender owner) ERR_UNAUTHORIZED) + (ft-burn? miamicoin amount owner) + ) +) + +;; checks if caller is Auth contract +(define-private (is-authorized-auth) + (is-eq contract-caller 'ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-auth-v2) +) + +;; SEND-MANY + +(define-public (send-many (recipients (list 200 { to: principal, amount: uint, memo: (optional (buff 34)) }))) + (fold check-err + (map send-token recipients) + (ok true) + ) +) + +(define-private (check-err (result (response bool uint)) (prior (response bool uint))) + (match prior ok-value + result + err-value (err err-value) + ) +) + +(define-private (send-token (recipient { to: principal, amount: uint, memo: (optional (buff 34)) })) + (send-token-with-memo (get amount recipient) (get to recipient) (get memo recipient)) +) + +(define-private (send-token-with-memo (amount uint) (to principal) (memo (optional (buff 34)))) + (let + ( + (transferOk (try! (transfer amount tx-sender to memo))) + ) + (ok transferOk) + ) +) diff --git a/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-token-v2.json b/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-token-v2.json new file mode 100644 index 0000000..89c8a70 --- /dev/null +++ b/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-token-v2.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.nft-trait.clar b/.cache/requirements/ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.nft-trait.clar new file mode 100644 index 0000000..5dd3646 --- /dev/null +++ b/.cache/requirements/ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.nft-trait.clar @@ -0,0 +1,15 @@ +(define-trait nft-trait + ( + ;; Last token ID, limited to uint range + (get-last-token-id () (response uint uint)) + + ;; URI for metadata associated with the token + (get-token-uri (uint) (response (optional (string-ascii 256)) uint)) + + ;; Owner of a given token identifier + (get-owner (uint) (response (optional principal) uint)) + + ;; Transfer from the sender to a new principal + (transfer (uint principal principal) (response bool uint)) + ) +) \ No newline at end of file diff --git a/.cache/requirements/ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.nft-trait.json b/.cache/requirements/ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.nft-trait.json new file mode 100644 index 0000000..11162a1 --- /dev/null +++ b/.cache/requirements/ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.nft-trait.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch20", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.sip-010-trait-ft-standard.clar b/.cache/requirements/ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.sip-010-trait-ft-standard.clar new file mode 100644 index 0000000..ae299b1 --- /dev/null +++ b/.cache/requirements/ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.sip-010-trait-ft-standard.clar @@ -0,0 +1,24 @@ +(define-trait sip-010-trait + ( + ;; Transfer from the caller to a new principal + (transfer (uint principal principal (optional (buff 34))) (response bool uint)) + + ;; the human readable name of the token + (get-name () (response (string-ascii 32) uint)) + + ;; the ticker symbol, or empty if none + (get-symbol () (response (string-ascii 32) uint)) + + ;; the number of decimals used, e.g. 6 would mean 1_000_000 represents 1 token + (get-decimals () (response uint uint)) + + ;; the balance of the passed principal + (get-balance (principal) (response uint uint)) + + ;; the current total supply (which does not need to be a constant) + (get-total-supply () (response uint uint)) + + ;; an optional URI that represents metadata of this token + (get-token-uri () (response (optional (string-utf8 256)) uint)) + ) +) \ No newline at end of file diff --git a/.cache/requirements/ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.sip-010-trait-ft-standard.json b/.cache/requirements/ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.sip-010-trait-ft-standard.json new file mode 100644 index 0000000..89c8a70 --- /dev/null +++ b/.cache/requirements/ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.sip-010-trait-ft-standard.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-core-v2-trait.clar b/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-core-v2-trait.clar new file mode 100644 index 0000000..ebb6539 --- /dev/null +++ b/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-core-v2-trait.clar @@ -0,0 +1,47 @@ +;; CITYCOIN CORE TRAIT V2 + +(define-trait citycoin-core-v2 + ( + + (register-user ((optional (string-utf8 50))) + (response bool uint) + ) + + (mine-tokens (uint (optional (buff 34))) + (response bool uint) + ) + + (mine-many ((list 200 uint)) + (response bool uint) + ) + + (claim-mining-reward (uint) + (response bool uint) + ) + + (stack-tokens (uint uint) + (response bool uint) + ) + + (claim-stacking-reward (uint) + (response bool uint) + ) + + (set-city-wallet (principal) + (response bool uint) + ) + + (update-coinbase-amounts () + (response bool uint) + ) + + (update-coinbase-thresholds () + (response bool uint) + ) + + (shutdown-contract (uint) + (response bool uint) + ) + + ) +) diff --git a/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-core-v2-trait.json b/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-core-v2-trait.json new file mode 100644 index 0000000..89c8a70 --- /dev/null +++ b/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-core-v2-trait.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-token-v2-trait.clar b/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-token-v2-trait.clar new file mode 100644 index 0000000..794ebda --- /dev/null +++ b/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-token-v2-trait.clar @@ -0,0 +1,35 @@ +;; CITYCOIN TOKEN TRAIT + +(define-trait citycoin-token-v2 + ( + + (activate-token (principal uint) + (response bool uint) + ) + + (set-token-uri ((optional (string-utf8 256))) + (response bool uint) + ) + + (mint (uint principal) + (response bool uint) + ) + + (burn (uint principal) + (response bool uint) + ) + + (send-many ((list 200 { to: principal, amount: uint, memo: (optional (buff 34)) })) + (response bool uint) + ) + + (update-coinbase-thresholds (uint uint uint uint uint) + (response bool uint) + ) + + (update-coinbase-amounts (uint uint uint uint uint uint uint) + (response bool uint) + ) + + ) +) diff --git a/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-token-v2-trait.json b/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-token-v2-trait.json new file mode 100644 index 0000000..89c8a70 --- /dev/null +++ b/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-token-v2-trait.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-vrf-v2.clar b/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-vrf-v2.clar new file mode 100644 index 0000000..1278c67 --- /dev/null +++ b/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-vrf-v2.clar @@ -0,0 +1,69 @@ +;; CITYCOIN VRF CONTRACT V2 +;; CityCoins Protocol Version 2.0.0 + +;; ERROR CODES + +(define-constant ERR_FAIL (err u3000)) + +;; CONFIGURATION + +(define-map RandomUintAtBlock uint uint) + +;; PUBLIC FUNCTIONS + +;; returns the saved random integer +;; or, calculates and saves it to the map +;; #[allow(unchecked_data)] +(define-public (get-save-rnd (block uint)) + (match (map-get? RandomUintAtBlock block) + rnd (ok rnd) + (match (read-rnd block) + rnd (begin (map-set RandomUintAtBlock block rnd) (ok rnd)) + err-val (err err-val) + ) + ) +) + +;; returns the saved random integer +;; or, calculates and returns the value +(define-read-only (get-rnd (block uint)) + (match (map-get? RandomUintAtBlock block) + rnd (ok rnd) + (read-rnd block) + ) +) + +;; PRIVATE FUNCTIONS + +(define-private (read-rnd (block uint)) + (ok (lower-16-le (unwrap! (get-block-info? vrf-seed block) ERR_FAIL))) +) + +(define-private (lower-16-le (vrfSeed (buff 32))) + (+ + (lower-16-le-inner (element-at vrfSeed u16) u15) + (lower-16-le-inner (element-at vrfSeed u17) u14) + (lower-16-le-inner (element-at vrfSeed u18) u13) + (lower-16-le-inner (element-at vrfSeed u19) u12) + (lower-16-le-inner (element-at vrfSeed u20) u11) + (lower-16-le-inner (element-at vrfSeed u21) u10) + (lower-16-le-inner (element-at vrfSeed u22) u9) + (lower-16-le-inner (element-at vrfSeed u23) u8) + (lower-16-le-inner (element-at vrfSeed u24) u7) + (lower-16-le-inner (element-at vrfSeed u25) u6) + (lower-16-le-inner (element-at vrfSeed u26) u5) + (lower-16-le-inner (element-at vrfSeed u27) u4) + (lower-16-le-inner (element-at vrfSeed u28) u3) + (lower-16-le-inner (element-at vrfSeed u29) u2) + (lower-16-le-inner (element-at vrfSeed u30) u1) + (lower-16-le-inner (element-at vrfSeed u31) u0) + ) +) + +(define-private (lower-16-le-inner (byte (optional (buff 1))) (pos uint)) + (* (buff-to-u8 (unwrap-panic byte)) (pow u2 (* u8 pos))) +) + +(define-private (buff-to-u8 (byte (buff 1))) + (unwrap-panic (index-of 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff byte)) +) diff --git a/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-vrf-v2.json b/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-vrf-v2.json new file mode 100644 index 0000000..89c8a70 --- /dev/null +++ b/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-vrf-v2.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-auth-v2.clar b/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-auth-v2.clar new file mode 100644 index 0000000..74a1f8b --- /dev/null +++ b/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-auth-v2.clar @@ -0,0 +1,630 @@ +;; NEWYORKCITYCOIN AUTH CONTRACT V2 TESTNET +;; CityCoins Protocol Version 2.0.0 + +(define-constant CONTRACT_OWNER tx-sender) + +;; TRAIT DEFINITIONS + +(use-trait coreTraitV2 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-core-v2-trait.citycoin-core-v2) +(use-trait tokenTraitV2 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-token-v2-trait.citycoin-token-v2) + +;; ERRORS + +(define-constant ERR_UNKNOWN_JOB (err u6000)) +(define-constant ERR_UNAUTHORIZED (err u6001)) +(define-constant ERR_JOB_IS_ACTIVE (err u6002)) +(define-constant ERR_JOB_IS_NOT_ACTIVE (err u6003)) +(define-constant ERR_ALREADY_VOTED_THIS_WAY (err u6004)) +(define-constant ERR_JOB_IS_EXECUTED (err u6005)) +(define-constant ERR_JOB_IS_NOT_APPROVED (err u6006)) +(define-constant ERR_ARGUMENT_ALREADY_EXISTS (err u6007)) +(define-constant ERR_NO_ACTIVE_CORE_CONTRACT (err u6008)) +(define-constant ERR_CORE_CONTRACT_NOT_FOUND (err u6009)) +(define-constant ERR_UNKNOWN_ARGUMENT (err u6010)) +(define-constant ERR_INCORRECT_CONTRACT_STATE (err u6011)) +(define-constant ERR_CONTRACT_ALREADY_EXISTS (err u6012)) + +;; JOB MANAGEMENT + +(define-constant REQUIRED_APPROVALS u2) + +(define-data-var lastJobId uint u0) + +(define-map Jobs + uint + { + creator: principal, + name: (string-ascii 255), + target: principal, + approvals: uint, + disapprovals: uint, + isActive: bool, + isExecuted: bool + } +) + +(define-map JobApprovers + { jobId: uint, approver: principal } + bool +) + +(define-map Approvers + principal + bool +) + +(define-map ArgumentLastIdsByType + { jobId: uint, argumentType: (string-ascii 25) } + uint +) + +(define-map UIntArgumentsByName + { jobId: uint, argumentName: (string-ascii 255) } + { argumentId: uint, value: uint} +) + +(define-map UIntArgumentsById + { jobId: uint, argumentId: uint } + { argumentName: (string-ascii 255), value: uint } +) + +(define-map PrincipalArgumentsByName + { jobId: uint, argumentName: (string-ascii 255) } + { argumentId: uint, value: principal } +) + +(define-map PrincipalArgumentsById + { jobId: uint, argumentId: uint } + { argumentName: (string-ascii 255), value: principal } +) + +;; FUNCTIONS + +(define-read-only (get-last-job-id) + (var-get lastJobId) +) + +(define-public (create-job (name (string-ascii 255)) (target principal)) + (let + ( + (newJobId (+ (var-get lastJobId) u1)) + ) + (asserts! (is-approver tx-sender) ERR_UNAUTHORIZED) + (map-set Jobs + newJobId + { + creator: tx-sender, + name: name, + target: target, + approvals: u0, + disapprovals: u0, + isActive: false, + isExecuted: false + } + ) + (var-set lastJobId newJobId) + (ok newJobId) + ) +) + +(define-read-only (get-job (jobId uint)) + (map-get? Jobs jobId) +) + +(define-public (activate-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + ) + (asserts! (is-eq (get creator job) tx-sender) ERR_UNAUTHORIZED) + (asserts! (not (get isActive job)) ERR_JOB_IS_ACTIVE) + (map-set Jobs + jobId + (merge job { isActive: true }) + ) + (ok true) + ) +) + +(define-public (approve-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + (previousVote (map-get? JobApprovers { jobId: jobId, approver: tx-sender })) + ) + (asserts! (get isActive job) ERR_JOB_IS_NOT_ACTIVE) + (asserts! (is-approver tx-sender) ERR_UNAUTHORIZED) + ;; save vote + (map-set JobApprovers + { jobId: jobId, approver: tx-sender } + true + ) + (match previousVote approved + (begin + (asserts! (not approved) ERR_ALREADY_VOTED_THIS_WAY) + (map-set Jobs jobId + (merge job + { + approvals: (+ (get approvals job) u1), + disapprovals: (- (get disapprovals job) u1) + } + ) + ) + ) + ;; no previous vote + (map-set Jobs + jobId + (merge job { approvals: (+ (get approvals job) u1) } ) + ) + ) + (ok true) + ) +) + +(define-public (disapprove-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + (previousVote (map-get? JobApprovers { jobId: jobId, approver: tx-sender })) + ) + (asserts! (get isActive job) ERR_JOB_IS_NOT_ACTIVE) + (asserts! (is-approver tx-sender) ERR_UNAUTHORIZED) + ;; save vote + (map-set JobApprovers + { jobId: jobId, approver: tx-sender } + false + ) + (match previousVote approved + (begin + (asserts! approved ERR_ALREADY_VOTED_THIS_WAY) + (map-set Jobs jobId + (merge job + { + approvals: (- (get approvals job) u1), + disapprovals: (+ (get disapprovals job) u1) + } + ) + ) + ) + ;; no previous vote + (map-set Jobs + jobId + (merge job { disapprovals: (+ (get disapprovals job) u1) } ) + ) + ) + (ok true) + ) +) + +(define-read-only (is-job-approved (jobId uint)) + (match (get-job jobId) job + (>= (get approvals job) REQUIRED_APPROVALS) + false + ) +) + +(define-public (mark-job-as-executed (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + ) + (asserts! (get isActive job) ERR_JOB_IS_NOT_ACTIVE) + (asserts! (>= (get approvals job) REQUIRED_APPROVALS) ERR_JOB_IS_NOT_APPROVED) + (asserts! (is-eq (get target job) contract-caller) ERR_UNAUTHORIZED) + (asserts! (not (get isExecuted job)) ERR_JOB_IS_EXECUTED) + (map-set Jobs + jobId + (merge job { isExecuted: true }) + ) + (ok true) + ) +) + +(define-public (add-uint-argument (jobId uint) (argumentName (string-ascii 255)) (value uint)) + (let + ( + (argumentId (generate-argument-id jobId "uint")) + ) + (try! (guard-add-argument jobId)) + (asserts! + (and + (map-insert UIntArgumentsById + { jobId: jobId, argumentId: argumentId } + { argumentName: argumentName, value: value } + ) + (map-insert UIntArgumentsByName + { jobId: jobId, argumentName: argumentName } + { argumentId: argumentId, value: value} + ) + ) + ERR_ARGUMENT_ALREADY_EXISTS + ) + (ok true) + ) +) + +(define-read-only (get-uint-argument-by-name (jobId uint) (argumentName (string-ascii 255))) + (map-get? UIntArgumentsByName { jobId: jobId, argumentName: argumentName }) +) + +(define-read-only (get-uint-argument-by-id (jobId uint) (argumentId uint)) + (map-get? UIntArgumentsById { jobId: jobId, argumentId: argumentId }) +) + +(define-read-only (get-uint-value-by-name (jobId uint) (argumentName (string-ascii 255))) + (get value (get-uint-argument-by-name jobId argumentName)) +) + +(define-read-only (get-uint-value-by-id (jobId uint) (argumentId uint)) + (get value (get-uint-argument-by-id jobId argumentId)) +) + +(define-public (add-principal-argument (jobId uint) (argumentName (string-ascii 255)) (value principal)) + (let + ( + (argumentId (generate-argument-id jobId "principal")) + ) + (try! (guard-add-argument jobId)) + (asserts! + (and + (map-insert PrincipalArgumentsById + { jobId: jobId, argumentId: argumentId } + { argumentName: argumentName, value: value } + ) + (map-insert PrincipalArgumentsByName + { jobId: jobId, argumentName: argumentName } + { argumentId: argumentId, value: value} + ) + ) + ERR_ARGUMENT_ALREADY_EXISTS + ) + (ok true) + ) +) + +(define-read-only (get-principal-argument-by-name (jobId uint) (argumentName (string-ascii 255))) + (map-get? PrincipalArgumentsByName { jobId: jobId, argumentName: argumentName }) +) + +(define-read-only (get-principal-argument-by-id (jobId uint) (argumentId uint)) + (map-get? PrincipalArgumentsById { jobId: jobId, argumentId: argumentId }) +) + +(define-read-only (get-principal-value-by-name (jobId uint) (argumentName (string-ascii 255))) + (get value (get-principal-argument-by-name jobId argumentName)) +) + +(define-read-only (get-principal-value-by-id (jobId uint) (argumentId uint)) + (get value (get-principal-argument-by-id jobId argumentId)) +) + +;; PRIVATE FUNCTIONS + +(define-read-only (is-approver (user principal)) + (default-to false (map-get? Approvers user)) +) + +(define-private (generate-argument-id (jobId uint) (argumentType (string-ascii 25))) + (let + ( + (argumentId (+ (default-to u0 (map-get? ArgumentLastIdsByType { jobId: jobId, argumentType: argumentType })) u1)) + ) + (map-set ArgumentLastIdsByType + { jobId: jobId, argumentType: argumentType } + argumentId + ) + ;; return + argumentId + ) +) + +(define-private (guard-add-argument (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + ) + (asserts! (not (get isActive job)) ERR_JOB_IS_ACTIVE) + (asserts! (is-eq (get creator job) contract-caller) ERR_UNAUTHORIZED) + (ok true) + ) +) + +;; CONTRACT MANAGEMENT + +;; initial value for active core contract +;; set to deployer address at startup to prevent +;; circular dependency of core on auth +(define-data-var activeCoreContract principal CONTRACT_OWNER) +(define-data-var initialized bool false) + +;; core contract states +(define-constant STATE_DEPLOYED u0) +(define-constant STATE_ACTIVE u1) +(define-constant STATE_INACTIVE u2) + +;; core contract map +(define-map CoreContracts + principal + { + state: uint, + startHeight: uint, + endHeight: uint + } +) + +;; getter for active core contract +(define-read-only (get-active-core-contract) + (begin + (asserts! (not (is-eq (var-get activeCoreContract) CONTRACT_OWNER)) ERR_NO_ACTIVE_CORE_CONTRACT) + (ok (var-get activeCoreContract)) + ) +) + +;; getter for core contract map +(define-read-only (get-core-contract-info (targetContract principal)) + (let + ( + (coreContract (unwrap! (map-get? CoreContracts targetContract) ERR_CORE_CONTRACT_NOT_FOUND)) + ) + (ok coreContract) + ) +) + +;; one-time function to initialize contracts after all contracts are deployed +;; - check that deployer is calling this function +;; - check this contract is not activated already (one-time use) +;; - set initial map value for core contract v1 +;; - set cityWallet in core contract +;; - set intialized true +(define-public (initialize-contracts (coreContract )) + (let + ( + (coreContractAddress (contract-of coreContract)) + ) + (asserts! (is-eq contract-caller CONTRACT_OWNER) ERR_UNAUTHORIZED) + (asserts! (not (var-get initialized)) ERR_UNAUTHORIZED) + (map-set CoreContracts + coreContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (try! (contract-call? coreContract set-city-wallet (var-get cityWallet))) + (var-set initialized true) + (ok true) + ) +) + +(define-read-only (is-initialized) + (var-get initialized) +) + +;; function to activate core contract through registration +;; - check that target is in core contract map +;; - check that caller is core contract +;; - check that target is in STATE_DEPLOYED +;; - set active in core contract map +;; - set as activeCoreContract +(define-public (activate-core-contract (targetContract principal) (stacksHeight uint)) + (let + ( + (coreContract (unwrap! (map-get? CoreContracts targetContract) ERR_CORE_CONTRACT_NOT_FOUND)) + ) + (asserts! (is-eq (get state coreContract) STATE_DEPLOYED) ERR_INCORRECT_CONTRACT_STATE) + (asserts! (is-eq contract-caller targetContract) ERR_UNAUTHORIZED) + (map-set CoreContracts + targetContract + { + state: STATE_ACTIVE, + startHeight: stacksHeight, + endHeight: u0 + }) + (var-set activeCoreContract targetContract) + (ok true) + ) +) + +;; protected function to update core contract +(define-public (upgrade-core-contract (oldContract ) (newContract )) + (let + ( + (oldContractAddress (contract-of oldContract)) + (oldContractMap (unwrap! (map-get? CoreContracts oldContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + (newContractAddress (contract-of newContract)) + ) + (asserts! (not (is-eq oldContractAddress newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (is-none (map-get? CoreContracts newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + (map-set CoreContracts + oldContractAddress + { + state: STATE_INACTIVE, + startHeight: (get startHeight oldContractMap), + endHeight: block-height + }) + (map-set CoreContracts + newContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (var-set activeCoreContract newContractAddress) + (try! (contract-call? oldContract shutdown-contract block-height)) + (try! (contract-call? newContract set-city-wallet (var-get cityWallet))) + (ok true) + ) +) + +(define-public (execute-upgrade-core-contract-job (jobId uint) (oldContract ) (newContract )) + (let + ( + (oldContractArg (unwrap! (get-principal-value-by-name jobId "oldContract") ERR_UNKNOWN_ARGUMENT)) + (newContractArg (unwrap! (get-principal-value-by-name jobId "newContract") ERR_UNKNOWN_ARGUMENT)) + (oldContractAddress (contract-of oldContract)) + (oldContractMap (unwrap! (map-get? CoreContracts oldContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + (newContractAddress (contract-of newContract)) + ) + (asserts! (not (is-eq oldContractAddress newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (is-none (map-get? CoreContracts newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (and (is-eq oldContractArg oldContractAddress) (is-eq newContractArg newContractAddress)) ERR_UNAUTHORIZED) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (map-set CoreContracts + oldContractAddress + { + state: STATE_INACTIVE, + startHeight: (get startHeight oldContractMap), + endHeight: block-height + }) + (map-set CoreContracts + newContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (var-set activeCoreContract newContractAddress) + (try! (contract-call? oldContract shutdown-contract block-height)) + (try! (contract-call? newContract set-city-wallet (var-get cityWallet))) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; CITY WALLET MANAGEMENT + +;; initial value for city wallet +(define-data-var cityWallet principal 'ST7G6VDV48CXXSP6J2B4RRCKTFJ5NK3PBZSD3YW5) + +;; returns city wallet principal +(define-read-only (get-city-wallet) + (ok (var-get cityWallet)) +) + +;; protected function to update city wallet variable +(define-public (set-city-wallet (targetContract ) (newCityWallet principal)) + (let + ( + (coreContractAddress (contract-of targetContract)) + (coreContract (unwrap! (map-get? CoreContracts coreContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + ) + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + (asserts! (is-eq coreContractAddress (var-get activeCoreContract)) ERR_UNAUTHORIZED) + (var-set cityWallet newCityWallet) + (try! (contract-call? targetContract set-city-wallet newCityWallet)) + (ok true) + ) +) + +(define-public (execute-set-city-wallet-job (jobId uint) (targetContract )) + (let + ( + (coreContractAddress (contract-of targetContract)) + (coreContract (unwrap! (map-get? CoreContracts coreContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + (newCityWallet (unwrap! (get-principal-value-by-name jobId "newCityWallet") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (asserts! (is-eq coreContractAddress (var-get activeCoreContract)) ERR_UNAUTHORIZED) + (var-set cityWallet newCityWallet) + (try! (contract-call? targetContract set-city-wallet newCityWallet)) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; check if contract caller is city wallet +(define-private (is-authorized-city) + (is-eq contract-caller (var-get cityWallet)) +) + +;; TOKEN MANAGEMENT + +(define-public (set-token-uri (targetContract ) (newUri (optional (string-utf8 256)))) + (begin + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + (as-contract (try! (contract-call? targetContract set-token-uri newUri))) + (ok true) + ) +) + +;; COINBASE THRESHOLDS + +(define-public (update-coinbase-thresholds (targetCore ) (targetToken ) (threshold1 uint) (threshold2 uint) (threshold3 uint) (threshold4 uint) (threshold5 uint)) + (begin + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + ;; update in token contract + (as-contract (try! (contract-call? targetToken update-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5))) + ;; update core contract based on token contract + (as-contract (try! (contract-call? targetCore update-coinbase-thresholds))) + (ok true) + ) +) + +(define-public (execute-update-coinbase-thresholds-job (jobId uint) (targetCore ) (targetToken )) + (let + ( + (threshold1 (unwrap! (get-uint-value-by-name jobId "threshold1") ERR_UNKNOWN_ARGUMENT)) + (threshold2 (unwrap! (get-uint-value-by-name jobId "threshold2") ERR_UNKNOWN_ARGUMENT)) + (threshold3 (unwrap! (get-uint-value-by-name jobId "threshold3") ERR_UNKNOWN_ARGUMENT)) + (threshold4 (unwrap! (get-uint-value-by-name jobId "threshold4") ERR_UNKNOWN_ARGUMENT)) + (threshold5 (unwrap! (get-uint-value-by-name jobId "threshold5") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (as-contract (try! (contract-call? targetToken update-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5))) + (as-contract (try! (contract-call? targetCore update-coinbase-thresholds))) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; COINBASE AMOUNTS (REWARDS) + +(define-public (update-coinbase-amounts (targetCore ) (targetToken ) (amountBonus uint) (amount1 uint) (amount2 uint) (amount3 uint) (amount4 uint) (amount5 uint) (amountDefault uint)) + (begin + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + ;; update in token contract + (as-contract (try! (contract-call? targetToken update-coinbase-amounts amountBonus amount1 amount2 amount3 amount4 amount5 amountDefault))) + ;; update core contract based on token contract + (as-contract (try! (contract-call? targetCore update-coinbase-amounts))) + (ok true) + ) +) + +(define-public (execute-update-coinbase-amounts-job (jobId uint) (targetCore ) (targetToken )) + (let + ( + (amountBonus (unwrap! (get-uint-value-by-name jobId "amountBonus") ERR_UNKNOWN_ARGUMENT)) + (amount1 (unwrap! (get-uint-value-by-name jobId "amount1") ERR_UNKNOWN_ARGUMENT)) + (amount2 (unwrap! (get-uint-value-by-name jobId "amount2") ERR_UNKNOWN_ARGUMENT)) + (amount3 (unwrap! (get-uint-value-by-name jobId "amount3") ERR_UNKNOWN_ARGUMENT)) + (amount4 (unwrap! (get-uint-value-by-name jobId "amount4") ERR_UNKNOWN_ARGUMENT)) + (amount5 (unwrap! (get-uint-value-by-name jobId "amount5") ERR_UNKNOWN_ARGUMENT)) + (amountDefault (unwrap! (get-uint-value-by-name jobId "amountDefault") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (as-contract (try! (contract-call? targetToken update-coinbase-amounts amountBonus amount1 amount2 amount3 amount4 amount5 amountDefault))) + (as-contract (try! (contract-call? targetCore update-coinbase-amounts))) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; APPROVERS MANAGEMENT + +(define-public (execute-replace-approver-job (jobId uint)) + (let + ( + (oldApprover (unwrap! (get-principal-value-by-name jobId "oldApprover") ERR_UNKNOWN_ARGUMENT)) + (newApprover (unwrap! (get-principal-value-by-name jobId "newApprover") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (map-set Approvers oldApprover false) + (map-set Approvers newApprover true) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; CONTRACT INITIALIZATION + +(map-insert Approvers 'ST3AY0CM7SD9183QZ4Y7S2RGBZX9GQT54MJ6XY0BN true) +(map-insert Approvers 'ST2D06VFWWTNCWHVB2FJ9KJ3EB30HFRTHB1A4BSP3 true) +(map-insert Approvers 'ST113N3MMPZRMJJRZH6JTHA5CB7TBZH1EH4C22GFV true) +(map-insert Approvers 'ST8YRW1THF2XT8E45XXCGYKZH2B70HYH71VC7737 true) +(map-insert Approvers 'STX13Q7ZJDSFVDZMQ1PWDFGT4QSBMASRMCYE4NAP true) diff --git a/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-auth-v2.json b/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-auth-v2.json new file mode 100644 index 0000000..89c8a70 --- /dev/null +++ b/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-auth-v2.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-core-v2.clar b/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-core-v2.clar new file mode 100644 index 0000000..d41a3e0 --- /dev/null +++ b/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-core-v2.clar @@ -0,0 +1,1007 @@ +;; NEWYORKCITYCOIN CORE CONTRACT V2 TESTNET +;; CityCoins Protocol Version 2.0.0 + +;; GENERAL CONFIGURATION + +(impl-trait 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-core-v2-trait.citycoin-core-v2) +(define-constant CONTRACT_OWNER tx-sender) + +;; ERROR CODES + +(define-constant ERR_UNAUTHORIZED (err u1000)) +(define-constant ERR_USER_ALREADY_REGISTERED (err u1001)) +(define-constant ERR_USER_NOT_FOUND (err u1002)) +(define-constant ERR_USER_ID_NOT_FOUND (err u1003)) +(define-constant ERR_ACTIVATION_THRESHOLD_REACHED (err u1004)) +(define-constant ERR_CONTRACT_NOT_ACTIVATED (err u1005)) +(define-constant ERR_USER_ALREADY_MINED (err u1006)) +(define-constant ERR_INSUFFICIENT_COMMITMENT (err u1007)) +(define-constant ERR_INSUFFICIENT_BALANCE (err u1008)) +(define-constant ERR_USER_DID_NOT_MINE_IN_BLOCK (err u1009)) +(define-constant ERR_CLAIMED_BEFORE_MATURITY (err u1010)) +(define-constant ERR_NO_MINERS_AT_BLOCK (err u1011)) +(define-constant ERR_REWARD_ALREADY_CLAIMED (err u1012)) +(define-constant ERR_MINER_DID_NOT_WIN (err u1013)) +(define-constant ERR_NO_VRF_SEED_FOUND (err u1014)) +(define-constant ERR_STACKING_NOT_AVAILABLE (err u1015)) +(define-constant ERR_CANNOT_STACK (err u1016)) +(define-constant ERR_REWARD_CYCLE_NOT_COMPLETED (err u1017)) +(define-constant ERR_NOTHING_TO_REDEEM (err u1018)) +(define-constant ERR_UNABLE_TO_FIND_CITY_WALLET (err u1019)) +(define-constant ERR_CLAIM_IN_WRONG_CONTRACT (err u1020)) +(define-constant ERR_BLOCK_HEIGHT_IN_PAST (err u1021)) +(define-constant ERR_COINBASE_AMOUNTS_NOT_FOUND (err u1022)) + +;; CITY WALLET MANAGEMENT + +;; initial value for city wallet, set to this contract until initialized +(define-data-var cityWallet principal 'STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-core-v2) + +;; returns set city wallet principal +(define-read-only (get-city-wallet) + (var-get cityWallet) +) + +;; protected function to update city wallet variable +(define-public (set-city-wallet (newCityWallet principal)) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (ok (var-set cityWallet newCityWallet)) + ) +) + +;; REGISTRATION + +(define-constant NEWYORKCITYCOIN_ACTIVATION_HEIGHT u37449) +(define-data-var activationBlock uint u340282366920938463463374607431768211455) +(define-data-var activationDelay uint u1) ;; TESTNET: set to 1 block +(define-data-var activationReached bool false) +(define-data-var activationTarget uint u0) +(define-data-var activationThreshold uint u2) ;; TESTNET: set to 2 users +(define-data-var usersNonce uint u0) + +;; returns Stacks block height registration was activated at plus activationDelay +(define-read-only (get-activation-block) + (begin + (asserts! (get-activation-status) ERR_CONTRACT_NOT_ACTIVATED) + (ok (var-get activationBlock)) + ) +) + +;; returns activation delay +(define-read-only (get-activation-delay) + (var-get activationDelay) +) + +;; returns activation status as boolean +(define-read-only (get-activation-status) + (var-get activationReached) +) + +;; returns activation target +(define-read-only (get-activation-target) + (begin + (asserts! (get-activation-status) ERR_CONTRACT_NOT_ACTIVATED) + (ok (var-get activationTarget)) + ) +) + +;; returns activation threshold +(define-read-only (get-activation-threshold) + (var-get activationThreshold) +) + +;; returns number of registered users, used for activation and tracking user IDs +(define-read-only (get-registered-users-nonce) + (var-get usersNonce) +) + +;; store user principal by user id +(define-map Users + uint + principal +) + +;; store user id by user principal +(define-map UserIds + principal + uint +) + +;; returns (some userId) or none +(define-read-only (get-user-id (user principal)) + (map-get? UserIds user) +) + +;; returns (some userPrincipal) or none +(define-read-only (get-user (userId uint)) + (map-get? Users userId) +) + +;; returns user ID if it has been created, or creates and returns new ID +(define-private (get-or-create-user-id (user principal)) + (match + (map-get? UserIds user) + value value + (let + ( + (newId (+ u1 (var-get usersNonce))) + ) + (map-set Users newId user) + (map-set UserIds user newId) + (var-set usersNonce newId) + newId + ) + ) +) + +;; registers users that signal activation of contract until threshold is met +(define-public (register-user (memo (optional (string-utf8 50)))) + (let + ( + (newId (+ u1 (var-get usersNonce))) + (threshold (var-get activationThreshold)) + (initialized (contract-call? 'STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-auth-v2 is-initialized)) + ) + + (asserts! initialized ERR_UNAUTHORIZED) + + (asserts! (is-none (map-get? UserIds tx-sender)) + ERR_USER_ALREADY_REGISTERED) + + (asserts! (<= newId threshold) + ERR_ACTIVATION_THRESHOLD_REACHED) + + (if (is-some memo) + (print memo) + none + ) + + (get-or-create-user-id tx-sender) + + (if (is-eq newId threshold) + (let + ( + (activationTargetBlock (+ block-height (var-get activationDelay))) + ) + (try! (contract-call? 'STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-auth-v2 activate-core-contract (as-contract tx-sender) activationTargetBlock)) + (try! (contract-call? 'STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-token-v2 activate-token (as-contract tx-sender) NEWYORKCITYCOIN_ACTIVATION_HEIGHT)) + (try! (set-coinbase-thresholds)) + (try! (set-coinbase-amounts)) + (var-set activationReached true) + (var-set activationBlock NEWYORKCITYCOIN_ACTIVATION_HEIGHT) + (var-set activationTarget activationTargetBlock) + (ok true) + ) + (ok true) + ) + ) +) + +;; MINING CONFIGURATION + +;; define split to custodied wallet address for the city +(define-constant SPLIT_CITY_PCT u30) + +;; how long a miner must wait before block winner can claim their minted tokens +(define-data-var tokenRewardMaturity uint u10) ;; TESTNET: set to 10 blocks + +;; At a given Stacks block height: +;; - how many miners were there +;; - what was the total amount submitted +;; - what was the total amount submitted to the city +;; - what was the total amount submitted to Stackers +;; - was the block reward claimed +(define-map MiningStatsAtBlock + uint + { + minersCount: uint, + amount: uint, + amountToCity: uint, + amountToStackers: uint, + rewardClaimed: bool + } +) + +;; returns map MiningStatsAtBlock at a given Stacks block height if it exists +(define-read-only (get-mining-stats-at-block (stacksHeight uint)) + (map-get? MiningStatsAtBlock stacksHeight) +) + +;; returns map MiningStatsAtBlock at a given Stacks block height +;; or, an empty structure +(define-read-only (get-mining-stats-at-block-or-default (stacksHeight uint)) + (default-to { + minersCount: u0, + amount: u0, + amountToCity: u0, + amountToStackers: u0, + rewardClaimed: false + } + (map-get? MiningStatsAtBlock stacksHeight) + ) +) + +;; At a given Stacks block height and user ID: +;; - what is their ustx commitment +;; - what are the low/high values (used for VRF) +(define-map MinersAtBlock + { + stacksHeight: uint, + userId: uint + } + { + ustx: uint, + lowValue: uint, + highValue: uint, + winner: bool + } +) + +;; returns true if a given miner has already mined at a given block height +(define-read-only (has-mined-at-block (stacksHeight uint) (userId uint)) + (is-some + (map-get? MinersAtBlock { stacksHeight: stacksHeight, userId: userId }) + ) +) + +;; returns map MinersAtBlock at a given Stacks block height for a user ID +(define-read-only (get-miner-at-block (stacksHeight uint) (userId uint)) + (map-get? MinersAtBlock { stacksHeight: stacksHeight, userId: userId }) +) + +;; returns map MinersAtBlock at a given Stacks block height for a user ID +;; or, an empty structure +(define-read-only (get-miner-at-block-or-default (stacksHeight uint) (userId uint)) + (default-to { + highValue: u0, + lowValue: u0, + ustx: u0, + winner: false + } + (map-get? MinersAtBlock { stacksHeight: stacksHeight, userId: userId })) +) + +;; At a given Stacks block height: +;; - what is the max highValue from MinersAtBlock (used for VRF) +(define-map MinersAtBlockHighValue + uint + uint +) + +;; returns last high value from map MinersAtBlockHighValue +(define-read-only (get-last-high-value-at-block (stacksHeight uint)) + (default-to u0 + (map-get? MinersAtBlockHighValue stacksHeight)) +) + +;; At a given Stacks block height: +;; - what is the userId of miner who won this block +(define-map BlockWinnerIds + uint + uint +) + +(define-read-only (get-block-winner-id (stacksHeight uint)) + (map-get? BlockWinnerIds stacksHeight) +) + +;; MINING ACTIONS + +(define-public (mine-tokens (amountUstx uint) (memo (optional (buff 34)))) + (let + ( + (userId (get-or-create-user-id tx-sender)) + ) + (try! (mine-tokens-at-block userId block-height amountUstx memo)) + (ok true) + ) +) + +(define-public (mine-many (amounts (list 200 uint))) + (begin + (asserts! (is-activated) ERR_CONTRACT_NOT_ACTIVATED) + (asserts! (> (len amounts) u0) ERR_INSUFFICIENT_COMMITMENT) + (match (fold mine-single amounts (ok { userId: (get-or-create-user-id tx-sender), toStackers: u0, toCity: u0, stacksHeight: block-height })) + okReturn + (begin + (asserts! (>= (stx-get-balance tx-sender) (+ (get toStackers okReturn) (get toCity okReturn))) ERR_INSUFFICIENT_BALANCE) + (if (> (get toStackers okReturn ) u0) + (try! (stx-transfer? (get toStackers okReturn ) tx-sender (as-contract tx-sender))) + false + ) + (try! (stx-transfer? (get toCity okReturn) tx-sender (var-get cityWallet))) + (print { + firstBlock: block-height, + lastBlock: (- (+ block-height (len amounts)) u1) + }) + (ok true) + ) + errReturn (err errReturn) + ) + ) +) + +(define-private (mine-single + (amountUstx uint) + (return (response + { + userId: uint, + toStackers: uint, + toCity: uint, + stacksHeight: uint + } + uint + ))) + + (match return okReturn + (let + ( + (stacksHeight (get stacksHeight okReturn)) + (rewardCycle (default-to u0 (get-reward-cycle stacksHeight))) + (stackingActive (stacking-active-at-cycle rewardCycle)) + (toCity + (if stackingActive + (/ (* SPLIT_CITY_PCT amountUstx) u100) + amountUstx + ) + ) + (toStackers (- amountUstx toCity)) + ) + (asserts! (not (has-mined-at-block stacksHeight (get userId okReturn))) ERR_USER_ALREADY_MINED) + (asserts! (> amountUstx u0) ERR_INSUFFICIENT_COMMITMENT) + (try! (set-tokens-mined (get userId okReturn) stacksHeight amountUstx toStackers toCity)) + (ok (merge okReturn + { + toStackers: (+ (get toStackers okReturn) toStackers), + toCity: (+ (get toCity okReturn) toCity), + stacksHeight: (+ stacksHeight u1) + } + )) + ) + errReturn (err errReturn) + ) +) + +(define-private (mine-tokens-at-block (userId uint) (stacksHeight uint) (amountUstx uint) (memo (optional (buff 34)))) + (let + ( + (rewardCycle (default-to u0 (get-reward-cycle stacksHeight))) + (stackingActive (stacking-active-at-cycle rewardCycle)) + (toCity + (if stackingActive + (/ (* SPLIT_CITY_PCT amountUstx) u100) + amountUstx + ) + ) + (toStackers (- amountUstx toCity)) + ) + (asserts! (is-activated) ERR_CONTRACT_NOT_ACTIVATED) + (asserts! (not (has-mined-at-block stacksHeight userId)) ERR_USER_ALREADY_MINED) + (asserts! (> amountUstx u0) ERR_INSUFFICIENT_COMMITMENT) + (asserts! (>= (stx-get-balance tx-sender) amountUstx) ERR_INSUFFICIENT_BALANCE) + (try! (set-tokens-mined userId stacksHeight amountUstx toStackers toCity)) + (if (is-some memo) + (print memo) + none + ) + (if stackingActive + (try! (stx-transfer? toStackers tx-sender (as-contract tx-sender))) + false + ) + (try! (stx-transfer? toCity tx-sender (var-get cityWallet))) + (ok true) + ) +) + +(define-private (set-tokens-mined (userId uint) (stacksHeight uint) (amountUstx uint) (toStackers uint) (toCity uint)) + (let + ( + (blockStats (get-mining-stats-at-block-or-default stacksHeight)) + (newMinersCount (+ (get minersCount blockStats) u1)) + (minerLowVal (get-last-high-value-at-block stacksHeight)) + (rewardCycle (unwrap! (get-reward-cycle stacksHeight) + ERR_STACKING_NOT_AVAILABLE)) + (rewardCycleStats (get-stacking-stats-at-cycle-or-default rewardCycle)) + ) + (map-set MiningStatsAtBlock + stacksHeight + { + minersCount: newMinersCount, + amount: (+ (get amount blockStats) amountUstx), + amountToCity: (+ (get amountToCity blockStats) toCity), + amountToStackers: (+ (get amountToStackers blockStats) toStackers), + rewardClaimed: false + } + ) + (map-set MinersAtBlock + { + stacksHeight: stacksHeight, + userId: userId + } + { + ustx: amountUstx, + lowValue: (if (> minerLowVal u0) (+ minerLowVal u1) u0), + highValue: (+ minerLowVal amountUstx), + winner: false + } + ) + (map-set MinersAtBlockHighValue + stacksHeight + (+ minerLowVal amountUstx) + ) + (if (> toStackers u0) + (map-set StackingStatsAtCycle + rewardCycle + { + amountUstx: (+ (get amountUstx rewardCycleStats) toStackers), + amountToken: (get amountToken rewardCycleStats) + } + ) + false + ) + (ok true) + ) +) + +;; MINING REWARD CLAIM ACTIONS + +;; calls function to claim mining reward in active logic contract +(define-public (claim-mining-reward (minerBlockHeight uint)) + (begin + (asserts! (or (is-eq (var-get shutdownHeight) u0) (< minerBlockHeight (var-get shutdownHeight))) ERR_CLAIM_IN_WRONG_CONTRACT) + (try! (claim-mining-reward-at-block tx-sender block-height minerBlockHeight)) + (ok true) + ) +) + +;; Determine whether or not the given principal can claim the mined tokens at a particular block height, +;; given the miners record for that block height, a random sample, and the current block height. +(define-private (claim-mining-reward-at-block (user principal) (stacksHeight uint) (minerBlockHeight uint)) + (let + ( + (maturityHeight (+ (var-get tokenRewardMaturity) minerBlockHeight)) + (userId (unwrap! (get-user-id user) ERR_USER_ID_NOT_FOUND)) + (blockStats (unwrap! (get-mining-stats-at-block minerBlockHeight) ERR_NO_MINERS_AT_BLOCK)) + (minerStats (unwrap! (get-miner-at-block minerBlockHeight userId) ERR_USER_DID_NOT_MINE_IN_BLOCK)) + (isMature (asserts! (> stacksHeight maturityHeight) ERR_CLAIMED_BEFORE_MATURITY)) + (vrfSample (unwrap! (contract-call? 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-vrf-v2 get-save-rnd maturityHeight) ERR_NO_VRF_SEED_FOUND)) + (commitTotal (get-last-high-value-at-block minerBlockHeight)) + (winningValue (mod vrfSample commitTotal)) + ) + (asserts! (not (get rewardClaimed blockStats)) ERR_REWARD_ALREADY_CLAIMED) + (asserts! (and (>= winningValue (get lowValue minerStats)) (<= winningValue (get highValue minerStats))) + ERR_MINER_DID_NOT_WIN) + (try! (set-mining-reward-claimed userId minerBlockHeight)) + (ok true) + ) +) + +(define-private (set-mining-reward-claimed (userId uint) (minerBlockHeight uint)) + (let + ( + (blockStats (get-mining-stats-at-block-or-default minerBlockHeight)) + (minerStats (get-miner-at-block-or-default minerBlockHeight userId)) + (user (unwrap! (get-user userId) ERR_USER_NOT_FOUND)) + ) + (map-set MiningStatsAtBlock + minerBlockHeight + { + minersCount: (get minersCount blockStats), + amount: (get amount blockStats), + amountToCity: (get amountToCity blockStats), + amountToStackers: (get amountToStackers blockStats), + rewardClaimed: true + } + ) + (map-set MinersAtBlock + { + stacksHeight: minerBlockHeight, + userId: userId + } + { + ustx: (get ustx minerStats), + lowValue: (get lowValue minerStats), + highValue: (get highValue minerStats), + winner: true + } + ) + (map-set BlockWinnerIds + minerBlockHeight + userId + ) + (try! (mint-coinbase user minerBlockHeight)) + (ok true) + ) +) + +(define-read-only (is-block-winner (user principal) (minerBlockHeight uint)) + (is-block-winner-and-can-claim user minerBlockHeight false) +) + +(define-read-only (can-claim-mining-reward (user principal) (minerBlockHeight uint)) + (is-block-winner-and-can-claim user minerBlockHeight true) +) + +(define-private (is-block-winner-and-can-claim (user principal) (minerBlockHeight uint) (testCanClaim bool)) + (let + ( + (userId (unwrap! (get-user-id user) false)) + (blockStats (unwrap! (get-mining-stats-at-block minerBlockHeight) false)) + (minerStats (unwrap! (get-miner-at-block minerBlockHeight userId) false)) + (maturityHeight (+ (var-get tokenRewardMaturity) minerBlockHeight)) + (vrfSample (unwrap! (contract-call? 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-vrf-v2 get-rnd maturityHeight) false)) + (commitTotal (get-last-high-value-at-block minerBlockHeight)) + (winningValue (mod vrfSample commitTotal)) + ) + (if (and (>= winningValue (get lowValue minerStats)) (<= winningValue (get highValue minerStats))) + (if testCanClaim (not (get rewardClaimed blockStats)) true) + false + ) + ) +) + +;; STACKING CONFIGURATION + +(define-constant MAX_REWARD_CYCLES u32) +(define-constant REWARD_CYCLE_INDEXES (list u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11 u12 u13 u14 u15 u16 u17 u18 u19 u20 u21 u22 u23 u24 u25 u26 u27 u28 u29 u30 u31)) + +;; how long a reward cycle is +(define-data-var rewardCycleLength uint u20) ;; TESTNET: set to 20 blocks + +;; At a given reward cycle: +;; - how many Stackers were there +;; - what is the total uSTX submitted by miners +;; - what is the total amount of tokens stacked +(define-map StackingStatsAtCycle + uint + { + amountUstx: uint, + amountToken: uint + } +) + +;; returns the total stacked tokens and committed uSTX for a given reward cycle +(define-read-only (get-stacking-stats-at-cycle (rewardCycle uint)) + (map-get? StackingStatsAtCycle rewardCycle) +) + +;; returns the total stacked tokens and committed uSTX for a given reward cycle +;; or, an empty structure +(define-read-only (get-stacking-stats-at-cycle-or-default (rewardCycle uint)) + (default-to { amountUstx: u0, amountToken: u0 } + (map-get? StackingStatsAtCycle rewardCycle)) +) + +;; At a given reward cycle and user ID: +;; - what is the total tokens Stacked? +;; - how many tokens should be returned? (based on Stacking period) +(define-map StackerAtCycle + { + rewardCycle: uint, + userId: uint + } + { + amountStacked: uint, + toReturn: uint + } +) + +(define-read-only (get-stacker-at-cycle (rewardCycle uint) (userId uint)) + (map-get? StackerAtCycle { rewardCycle: rewardCycle, userId: userId }) +) + +(define-read-only (get-stacker-at-cycle-or-default (rewardCycle uint) (userId uint)) + (default-to { amountStacked: u0, toReturn: u0 } + (map-get? StackerAtCycle { rewardCycle: rewardCycle, userId: userId })) +) + +;; get the reward cycle for a given Stacks block height +(define-read-only (get-reward-cycle (stacksHeight uint)) + (let + ( + (firstStackingBlock (var-get activationBlock)) + (rcLen (var-get rewardCycleLength)) + ) + (if (>= stacksHeight firstStackingBlock) + (some (/ (- stacksHeight firstStackingBlock) rcLen)) + none) + ) +) + +;; determine if stacking is active in a given cycle +(define-read-only (stacking-active-at-cycle (rewardCycle uint)) + (is-some + (get amountToken (map-get? StackingStatsAtCycle rewardCycle)) + ) +) + +;; get the first Stacks block height for a given reward cycle. +(define-read-only (get-first-stacks-block-in-reward-cycle (rewardCycle uint)) + (+ (var-get activationBlock) (* (var-get rewardCycleLength) rewardCycle)) +) + +;; getter for get-entitled-stacking-reward that specifies block height +(define-read-only (get-stacking-reward (userId uint) (targetCycle uint)) + (get-entitled-stacking-reward userId targetCycle block-height) +) + +;; get uSTX a Stacker can claim, given reward cycle they stacked in and current block height +;; this method only returns a positive value if: +;; - the current block height is in a subsequent reward cycle +;; - the stacker actually locked up tokens in the target reward cycle +;; - the stacker locked up _enough_ tokens to get at least one uSTX +;; it is possible to Stack tokens and not receive uSTX: +;; - if no miners commit during this reward cycle +;; - the amount stacked by user is too few that you'd be entitled to less than 1 uSTX +(define-private (get-entitled-stacking-reward (userId uint) (targetCycle uint) (stacksHeight uint)) + (let + ( + (rewardCycleStats (get-stacking-stats-at-cycle-or-default targetCycle)) + (stackerAtCycle (get-stacker-at-cycle-or-default targetCycle userId)) + (totalUstxThisCycle (get amountUstx rewardCycleStats)) + (totalStackedThisCycle (get amountToken rewardCycleStats)) + (userStackedThisCycle (get amountStacked stackerAtCycle)) + ) + (match (get-reward-cycle stacksHeight) + currentCycle + (if (and (not (var-get isShutdown)) + (or (<= currentCycle targetCycle) (is-eq u0 userStackedThisCycle))) + ;; the contract is not shut down and + ;; this cycle hasn't finished + ;; or stacker contributed nothing + u0 + ;; (totalUstxThisCycle * userStackedThisCycle) / totalStackedThisCycle + (/ (* totalUstxThisCycle userStackedThisCycle) totalStackedThisCycle) + ) + ;; before first reward cycle + u0 + ) + ) +) + +;; STACKING ACTIONS + +(define-public (stack-tokens (amountTokens uint) (lockPeriod uint)) + (let + ( + (userId (get-or-create-user-id tx-sender)) + ) + (try! (stack-tokens-at-cycle tx-sender userId amountTokens block-height lockPeriod)) + (ok true) + ) +) + +(define-private (stack-tokens-at-cycle (user principal) (userId uint) (amountTokens uint) (startHeight uint) (lockPeriod uint)) + (let + ( + (currentCycle (unwrap! (get-reward-cycle startHeight) ERR_STACKING_NOT_AVAILABLE)) + (targetCycle (+ u1 currentCycle)) + (commitment { + stackerId: userId, + amount: amountTokens, + first: targetCycle, + last: (+ targetCycle lockPeriod) + }) + ) + (asserts! (is-activated) ERR_CONTRACT_NOT_ACTIVATED) + (asserts! (and (> lockPeriod u0) (<= lockPeriod MAX_REWARD_CYCLES)) + ERR_CANNOT_STACK) + (asserts! (> amountTokens u0) ERR_CANNOT_STACK) + (try! (contract-call? 'STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-token-v2 transfer amountTokens tx-sender (as-contract tx-sender) none)) + (print { + firstCycle: targetCycle, + lastCycle: (- (+ targetCycle lockPeriod) u1) + }) + (match (fold stack-tokens-closure REWARD_CYCLE_INDEXES (ok commitment)) + okValue (ok true) + errValue (err errValue) + ) + ) +) + +(define-private (stack-tokens-closure (rewardCycleIdx uint) + (commitmentResponse (response + { + stackerId: uint, + amount: uint, + first: uint, + last: uint + } + uint + ))) + + (match commitmentResponse + commitment + (let + ( + (stackerId (get stackerId commitment)) + (amountToken (get amount commitment)) + (firstCycle (get first commitment)) + (lastCycle (get last commitment)) + (targetCycle (+ firstCycle rewardCycleIdx)) + ) + (begin + (if (and (>= targetCycle firstCycle) (< targetCycle lastCycle)) + (begin + (if (is-eq targetCycle (- lastCycle u1)) + (set-tokens-stacked stackerId targetCycle amountToken amountToken) + (set-tokens-stacked stackerId targetCycle amountToken u0) + ) + true + ) + false + ) + commitmentResponse + ) + ) + errValue commitmentResponse + ) +) + +(define-private (set-tokens-stacked (userId uint) (targetCycle uint) (amountStacked uint) (toReturn uint)) + (let + ( + (rewardCycleStats (get-stacking-stats-at-cycle-or-default targetCycle)) + (stackerAtCycle (get-stacker-at-cycle-or-default targetCycle userId)) + ) + (map-set StackingStatsAtCycle + targetCycle + { + amountUstx: (get amountUstx rewardCycleStats), + amountToken: (+ amountStacked (get amountToken rewardCycleStats)) + } + ) + (map-set StackerAtCycle + { + rewardCycle: targetCycle, + userId: userId + } + { + amountStacked: (+ amountStacked (get amountStacked stackerAtCycle)), + toReturn: (+ toReturn (get toReturn stackerAtCycle)) + } + ) + ) +) + +;; STACKING REWARD CLAIMS + +;; calls function to claim stacking reward in active logic contract +(define-public (claim-stacking-reward (targetCycle uint)) + (begin + (try! (claim-stacking-reward-at-cycle tx-sender block-height targetCycle)) + (ok true) + ) +) + +(define-private (claim-stacking-reward-at-cycle (user principal) (stacksHeight uint) (targetCycle uint)) + (let + ( + (currentCycle (unwrap! (get-reward-cycle stacksHeight) ERR_STACKING_NOT_AVAILABLE)) + (userId (unwrap! (get-user-id user) ERR_USER_ID_NOT_FOUND)) + (entitledUstx (get-entitled-stacking-reward userId targetCycle stacksHeight)) + (stackerAtCycle (get-stacker-at-cycle-or-default targetCycle userId)) + (toReturn (get toReturn stackerAtCycle)) + ) + (asserts! (or + (is-eq true (var-get isShutdown)) + (> currentCycle targetCycle)) + ERR_REWARD_CYCLE_NOT_COMPLETED) + (asserts! (or (> toReturn u0) (> entitledUstx u0)) ERR_NOTHING_TO_REDEEM) + ;; disable ability to claim again + (map-set StackerAtCycle + { + rewardCycle: targetCycle, + userId: userId + } + { + amountStacked: u0, + toReturn: u0 + } + ) + ;; send back tokens if user was eligible + (if (> toReturn u0) + (try! (as-contract (contract-call? 'STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-token-v2 transfer toReturn tx-sender user none))) + true + ) + ;; send back rewards if user was eligible + (if (> entitledUstx u0) + (try! (as-contract (stx-transfer? entitledUstx tx-sender user))) + true + ) + (ok true) + ) +) + +;; TOKEN CONFIGURATION + +;; decimals and multiplier for token +(define-constant DECIMALS u6) +(define-constant MICRO_CITYCOINS (pow u10 DECIMALS)) + +;; bonus period length for increased coinbase rewards +(define-constant TOKEN_BONUS_PERIOD u10000) + +;; coinbase thresholds per halving, used to determine halvings +(define-data-var coinbaseThreshold1 uint u0) +(define-data-var coinbaseThreshold2 uint u0) +(define-data-var coinbaseThreshold3 uint u0) +(define-data-var coinbaseThreshold4 uint u0) +(define-data-var coinbaseThreshold5 uint u0) + +;; return coinbase thresholds if contract activated +(define-read-only (get-coinbase-thresholds) + (let + ( + (activated (get-activation-status)) + ) + (asserts! activated ERR_CONTRACT_NOT_ACTIVATED) + (ok { + coinbaseThreshold1: (var-get coinbaseThreshold1), + coinbaseThreshold2: (var-get coinbaseThreshold2), + coinbaseThreshold3: (var-get coinbaseThreshold3), + coinbaseThreshold4: (var-get coinbaseThreshold4), + coinbaseThreshold5: (var-get coinbaseThreshold5) + }) + ) +) + +;; set coinbase thresholds, used during activation +(define-private (set-coinbase-thresholds) + (let + ( + (coinbaseThresholds (try! (contract-call? 'STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-token-v2 get-coinbase-thresholds))) + ) + (var-set coinbaseThreshold1 (get coinbaseThreshold1 coinbaseThresholds)) + (var-set coinbaseThreshold2 (get coinbaseThreshold2 coinbaseThresholds)) + (var-set coinbaseThreshold3 (get coinbaseThreshold3 coinbaseThresholds)) + (var-set coinbaseThreshold4 (get coinbaseThreshold4 coinbaseThresholds)) + (var-set coinbaseThreshold5 (get coinbaseThreshold5 coinbaseThresholds)) + ;; print coinbase thresholds + (print { + coinbaseThreshold1: (var-get coinbaseThreshold1), + coinbaseThreshold2: (var-get coinbaseThreshold2), + coinbaseThreshold3: (var-get coinbaseThreshold3), + coinbaseThreshold4: (var-get coinbaseThreshold4), + coinbaseThreshold5: (var-get coinbaseThreshold5) + }) + (ok true) + ) +) + +;; guarded function for auth to update coinbase thresholds +(define-public (update-coinbase-thresholds) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (try! (set-coinbase-thresholds)) + (ok true) + ) +) + +;; coinbase rewards per threshold, used to determine rewards +(define-data-var coinbaseAmountBonus uint u0) +(define-data-var coinbaseAmount1 uint u0) +(define-data-var coinbaseAmount2 uint u0) +(define-data-var coinbaseAmount3 uint u0) +(define-data-var coinbaseAmount4 uint u0) +(define-data-var coinbaseAmount5 uint u0) +(define-data-var coinbaseAmountDefault uint u0) + +;; return coinbase amounts if contract activated +(define-read-only (get-coinbase-amounts) + (let + ( + (activated (get-activation-status)) + ) + (asserts! activated ERR_CONTRACT_NOT_ACTIVATED) + (ok { + coinbaseAmountBonus: (var-get coinbaseAmountBonus), + coinbaseAmount1: (var-get coinbaseAmount1), + coinbaseAmount2: (var-get coinbaseAmount2), + coinbaseAmount3: (var-get coinbaseAmount3), + coinbaseAmount4: (var-get coinbaseAmount4), + coinbaseAmount5: (var-get coinbaseAmount5), + coinbaseAmountDefault: (var-get coinbaseAmountDefault) + }) + ) +) + +;; set coinbase amounts, used during activation +(define-private (set-coinbase-amounts) + (let + ( + (coinbaseAmounts (unwrap! (contract-call? 'STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-token-v2 get-coinbase-amounts) ERR_COINBASE_AMOUNTS_NOT_FOUND)) + ) + (var-set coinbaseAmountBonus (get coinbaseAmountBonus coinbaseAmounts)) + (var-set coinbaseAmount1 (get coinbaseAmount1 coinbaseAmounts)) + (var-set coinbaseAmount2 (get coinbaseAmount2 coinbaseAmounts)) + (var-set coinbaseAmount3 (get coinbaseAmount3 coinbaseAmounts)) + (var-set coinbaseAmount4 (get coinbaseAmount4 coinbaseAmounts)) + (var-set coinbaseAmount5 (get coinbaseAmount5 coinbaseAmounts)) + (var-set coinbaseAmountDefault (get coinbaseAmountDefault coinbaseAmounts)) + ;; print coinbase amounts + (print { + coinbaseAmountBonus: (var-get coinbaseAmountBonus), + coinbaseAmount1: (var-get coinbaseAmount1), + coinbaseAmount2: (var-get coinbaseAmount2), + coinbaseAmount3: (var-get coinbaseAmount3), + coinbaseAmount4: (var-get coinbaseAmount4), + coinbaseAmount5: (var-get coinbaseAmount5), + coinbaseAmountDefault: (var-get coinbaseAmountDefault) + }) + (ok true) + ) +) + +;; guarded function for auth to update coinbase amounts +(define-public (update-coinbase-amounts) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (try! (set-coinbase-amounts)) + (ok true) + ) +) + +;; function for deciding how many tokens to mint, depending on when they were mined +(define-read-only (get-coinbase-amount (minerBlockHeight uint)) + (begin + ;; if contract is not active, return 0 + (asserts! (>= minerBlockHeight (var-get activationBlock)) u0) + ;; if contract is active, return based on emissions schedule + ;; defined in CCIP-008 https://github.com/citycoins/governance + (asserts! (> minerBlockHeight (var-get coinbaseThreshold1)) + (if (<= (- minerBlockHeight (var-get activationBlock)) TOKEN_BONUS_PERIOD) + ;; bonus reward for initial miners + (var-get coinbaseAmountBonus) + ;; standard reward until 1st halving + (var-get coinbaseAmount1) + ) + ) + ;; computations based on each halving threshold + (asserts! (> minerBlockHeight (var-get coinbaseThreshold2)) (var-get coinbaseAmount2)) + (asserts! (> minerBlockHeight (var-get coinbaseThreshold3)) (var-get coinbaseAmount3)) + (asserts! (> minerBlockHeight (var-get coinbaseThreshold4)) (var-get coinbaseAmount4)) + (asserts! (> minerBlockHeight (var-get coinbaseThreshold5)) (var-get coinbaseAmount5)) + ;; default value after 5th halving + (var-get coinbaseAmountDefault) + ) +) + +;; mint new tokens for claimant who won at given Stacks block height +(define-private (mint-coinbase (recipient principal) (stacksHeight uint)) + (as-contract (contract-call? 'STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-token-v2 mint (get-coinbase-amount stacksHeight) recipient)) +) + +;; UTILITIES + +(define-data-var shutdownHeight uint u0) +(define-data-var isShutdown bool false) + +;; stop mining and stacking operations +;; in preparation for a core upgrade +(define-public (shutdown-contract (stacksHeight uint)) + (begin + ;; make sure block height is in the future + (asserts! (>= stacksHeight block-height) ERR_BLOCK_HEIGHT_IN_PAST) + ;; only allow shutdown request from AUTH + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + ;; set variables to disable mining/stacking in CORE + (var-set activationReached false) + (var-set shutdownHeight stacksHeight) + ;; set variable to allow for all stacking claims + (var-set isShutdown true) + (ok true) + ) +) + +;; checks if caller is Auth contract +(define-private (is-authorized-auth) + (is-eq contract-caller 'STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-auth-v2) +) + +;; checks if contract is fully activated to +;; enable mining and stacking functions +(define-private (is-activated) + (and (get-activation-status) (>= block-height (var-get activationTarget))) +) diff --git a/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-core-v2.json b/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-core-v2.json new file mode 100644 index 0000000..89c8a70 --- /dev/null +++ b/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-core-v2.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-token-v2.clar b/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-token-v2.clar new file mode 100644 index 0000000..30efc5f --- /dev/null +++ b/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-token-v2.clar @@ -0,0 +1,281 @@ +;; NEWYORKCITYCOIN TOKEN V2 CONTRACT TESTNET +;; CityCoins Protocol Version 2.0.0 + +;; TRAIT DEFINITIONS + +(impl-trait 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-token-v2-trait.citycoin-token-v2) + +;; ERROR CODES + +(define-constant ERR_UNAUTHORIZED (err u2000)) +(define-constant ERR_TOKEN_NOT_ACTIVATED (err u2001)) +(define-constant ERR_TOKEN_ALREADY_ACTIVATED (err u2002)) +(define-constant ERR_V1_BALANCE_NOT_FOUND (err u2003)) +(define-constant ERR_INVALID_COINBASE_THRESHOLD (err u2004)) +(define-constant ERR_INVALID_COINBASE_AMOUNT (err u2005)) + +;; SIP-010 DEFINITION + +(impl-trait 'ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.sip-010-trait-ft-standard.sip-010-trait) + +(define-fungible-token newyorkcitycoin) + +(define-constant DECIMALS u6) +(define-constant MICRO_CITYCOINS (pow u10 DECIMALS)) + +;; SIP-010 FUNCTIONS + +(define-public (transfer (amount uint) (from principal) (to principal) (memo (optional (buff 34)))) + (begin + (asserts! (is-eq from tx-sender) ERR_UNAUTHORIZED) + (if (is-some memo) + (print memo) + none + ) + (ft-transfer? newyorkcitycoin amount from to) + ) +) + +(define-read-only (get-name) + (ok "newyorkcitycoin") +) + +(define-read-only (get-symbol) + (ok "NYC") +) + +(define-read-only (get-decimals) + (ok DECIMALS) +) + +(define-read-only (get-balance (user principal)) + (ok (ft-get-balance newyorkcitycoin user)) +) + +(define-read-only (get-total-supply) + (ok (ft-get-supply newyorkcitycoin)) +) + +(define-read-only (get-token-uri) + (ok (var-get tokenUri)) +) + +;; TOKEN CONFIGURATION + +;; define bonus period and initial epoch length +(define-constant TOKEN_BONUS_PERIOD u10000) +(define-constant TOKEN_EPOCH_LENGTH u25000) + +;; once activated, activation cannot happen again +(define-data-var tokenActivated bool false) + +;; core contract states +(define-constant STATE_DEPLOYED u0) +(define-constant STATE_ACTIVE u1) +(define-constant STATE_INACTIVE u2) + +;; one-time function to activate the token +(define-public (activate-token (coreContract principal) (stacksHeight uint)) + (let + ( + (coreContractMap (try! (contract-call? 'STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-auth-v2 get-core-contract-info coreContract))) + (threshold1 (+ stacksHeight TOKEN_BONUS_PERIOD TOKEN_EPOCH_LENGTH)) ;; 35,000 blocks + (threshold2 (+ stacksHeight TOKEN_BONUS_PERIOD (* u3 TOKEN_EPOCH_LENGTH))) ;; 85,000 blocks + (threshold3 (+ stacksHeight TOKEN_BONUS_PERIOD (* u7 TOKEN_EPOCH_LENGTH))) ;; 185,000 blocks + (threshold4 (+ stacksHeight TOKEN_BONUS_PERIOD (* u15 TOKEN_EPOCH_LENGTH))) ;; 385,000 blocks + (threshold5 (+ stacksHeight TOKEN_BONUS_PERIOD (* u31 TOKEN_EPOCH_LENGTH))) ;; 785,000 blocks + ) + (asserts! (is-eq (get state coreContractMap) STATE_ACTIVE) ERR_UNAUTHORIZED) + (asserts! (not (var-get tokenActivated)) ERR_TOKEN_ALREADY_ACTIVATED) + (var-set tokenActivated true) + (try! (set-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5)) + (ok true) + ) +) + +;; COINBASE THRESHOLDS + +;; coinbase thresholds per halving, used to select coinbase rewards in core +;; initially set by register-user in core contract per CCIP-008 +(define-data-var coinbaseThreshold1 uint u0) +(define-data-var coinbaseThreshold2 uint u0) +(define-data-var coinbaseThreshold3 uint u0) +(define-data-var coinbaseThreshold4 uint u0) +(define-data-var coinbaseThreshold5 uint u0) + +;; return coinbase thresholds if token activated +(define-read-only (get-coinbase-thresholds) + (let + ( + (activated (var-get tokenActivated)) + ) + (asserts! activated ERR_TOKEN_NOT_ACTIVATED) + (ok { + coinbaseThreshold1: (var-get coinbaseThreshold1), + coinbaseThreshold2: (var-get coinbaseThreshold2), + coinbaseThreshold3: (var-get coinbaseThreshold3), + coinbaseThreshold4: (var-get coinbaseThreshold4), + coinbaseThreshold5: (var-get coinbaseThreshold5) + }) + ) +) + +(define-private (set-coinbase-thresholds (threshold1 uint) (threshold2 uint) (threshold3 uint) (threshold4 uint) (threshold5 uint)) + (begin + ;; check that all thresholds increase in value + (asserts! (and (> threshold1 u0) (> threshold2 threshold1) (> threshold3 threshold2) (> threshold4 threshold3) (> threshold5 threshold4)) ERR_INVALID_COINBASE_THRESHOLD) + ;; set coinbase thresholds + (var-set coinbaseThreshold1 threshold1) + (var-set coinbaseThreshold2 threshold2) + (var-set coinbaseThreshold3 threshold3) + (var-set coinbaseThreshold4 threshold4) + (var-set coinbaseThreshold5 threshold5) + ;; print coinbase thresholds + (print { + coinbaseThreshold1: threshold1, + coinbaseThreshold2: threshold2, + coinbaseThreshold3: threshold3, + coinbaseThreshold4: threshold4, + coinbaseThreshold5: threshold5 + }) + (ok true) + ) +) + +;; only accessible by auth +(define-public (update-coinbase-thresholds (threshold1 uint) (threshold2 uint) (threshold3 uint) (threshold4 uint) (threshold5 uint)) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (asserts! (var-get tokenActivated) ERR_TOKEN_NOT_ACTIVATED) + (try! (set-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5)) + (ok true) + ) +) + +;; COINBASE AMOUNTS (REWARDS) + +;; coinbase rewards per threshold per CCIP-008 +(define-data-var coinbaseAmountBonus uint (* MICRO_CITYCOINS u250000)) +(define-data-var coinbaseAmount1 uint (* MICRO_CITYCOINS u100000)) +(define-data-var coinbaseAmount2 uint (* MICRO_CITYCOINS u50000)) +(define-data-var coinbaseAmount3 uint (* MICRO_CITYCOINS u25000)) +(define-data-var coinbaseAmount4 uint (* MICRO_CITYCOINS u12500)) +(define-data-var coinbaseAmount5 uint (* MICRO_CITYCOINS u6250)) +(define-data-var coinbaseAmountDefault uint (* MICRO_CITYCOINS u3125)) + +;; return coinbase thresholds if token activated +(define-read-only (get-coinbase-amounts) + (ok { + coinbaseAmountBonus: (var-get coinbaseAmountBonus), + coinbaseAmount1: (var-get coinbaseAmount1), + coinbaseAmount2: (var-get coinbaseAmount2), + coinbaseAmount3: (var-get coinbaseAmount3), + coinbaseAmount4: (var-get coinbaseAmount4), + coinbaseAmount5: (var-get coinbaseAmount5), + coinbaseAmountDefault: (var-get coinbaseAmountDefault) + }) +) + +(define-private (set-coinbase-amounts (amountBonus uint) (amount1 uint) (amount2 uint) (amount3 uint) (amount4 uint) (amount5 uint) (amountDefault uint)) + (begin + ;; check that all amounts are greater than zero + (asserts! (and (> amountBonus u0) (> amount1 u0) (> amount2 u0) (> amount3 u0) (> amount4 u0) (> amount5 u0) (> amountDefault u0)) ERR_INVALID_COINBASE_AMOUNT) + ;; set coinbase amounts in token contract + (var-set coinbaseAmountBonus amountBonus) + (var-set coinbaseAmount1 amount1) + (var-set coinbaseAmount2 amount2) + (var-set coinbaseAmount3 amount3) + (var-set coinbaseAmount4 amount4) + (var-set coinbaseAmount5 amount5) + (var-set coinbaseAmountDefault amountDefault) + ;; print coinbase amounts + (print { + coinbaseAmountBonus: amountBonus, + coinbaseAmount1: amount1, + coinbaseAmount2: amount2, + coinbaseAmount3: amount3, + coinbaseAmount4: amount4, + coinbaseAmount5: amount5, + coinbaseAmountDefault: amountDefault + }) + (ok true) + ) +) + +;; only accessible by auth +(define-public (update-coinbase-amounts (amountBonus uint) (amount1 uint) (amount2 uint) (amount3 uint) (amount4 uint) (amount5 uint) (amountDefault uint)) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + ;; (asserts! (var-get tokenActivated) ERR_TOKEN_NOT_ACTIVATED) + (try! (set-coinbase-amounts amountBonus amount1 amount2 amount3 amount4 amount5 amountDefault)) + (ok true) + ) +) + +;; V1 TO V2 CONVERSION + +;; TESTNET: convert-to-v2 removed, no v1 + +;; UTILITIES + +(define-data-var tokenUri (optional (string-utf8 256)) (some u"https://cdn.citycoins.co/metadata/newyorkcitycoin.json")) + +;; set token URI to new value, only accessible by Auth +(define-public (set-token-uri (newUri (optional (string-utf8 256)))) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (ok (var-set tokenUri newUri)) + ) +) + +;; mint new tokens, only accessible by a Core contract +(define-public (mint (amount uint) (recipient principal)) + (let + ( + (coreContract (try! (contract-call? 'STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-auth-v2 get-core-contract-info contract-caller))) + ) + (ft-mint? newyorkcitycoin amount recipient) + ) +) + +;; burn tokens +(define-public (burn (amount uint) (owner principal)) + (begin + (asserts! (is-eq tx-sender owner) ERR_UNAUTHORIZED) + (ft-burn? newyorkcitycoin amount owner) + ) +) + +;; checks if caller is Auth contract +(define-private (is-authorized-auth) + (is-eq contract-caller 'STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-auth-v2) +) + +;; SEND-MANY + +(define-public (send-many (recipients (list 200 { to: principal, amount: uint, memo: (optional (buff 34)) }))) + (fold check-err + (map send-token recipients) + (ok true) + ) +) + +(define-private (check-err (result (response bool uint)) (prior (response bool uint))) + (match prior ok-value + result + err-value (err err-value) + ) +) + +(define-private (send-token (recipient { to: principal, amount: uint, memo: (optional (buff 34)) })) + (send-token-with-memo (get amount recipient) (get to recipient) (get memo recipient)) +) + +(define-private (send-token-with-memo (amount uint) (to principal) (memo (optional (buff 34)))) + (let + ( + (transferOk (try! (transfer amount tx-sender to memo))) + ) + (ok transferOk) + ) +) diff --git a/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-token-v2.json b/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-token-v2.json new file mode 100644 index 0000000..89c8a70 --- /dev/null +++ b/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-token-v2.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index c1cda78..9432455 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,16 @@ **/settings/Mainnet.toml **/settings/Testnet.toml -.cache/ + +# removing to resolve issue with testnet reset +# will use files obtained locally beforehand +# .cache/ +# still want to ignore these two +.cache/deps +.cache/gen +# .cache/requirements will upload contracts + .requirements/ /coverage_report history.txt -coverage.lcov \ No newline at end of file +coverage.lcov From c02cf909062267b049f2663e5f7e4365fc83ae21 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 12 Jun 2024 06:39:36 -0700 Subject: [PATCH 56/65] fix: update job name for better display of legacy test suite --- .github/workflows/clarinet-legacy.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/clarinet-legacy.yaml b/.github/workflows/clarinet-legacy.yaml index 53bffbe..2f2aae6 100644 --- a/.github/workflows/clarinet-legacy.yaml +++ b/.github/workflows/clarinet-legacy.yaml @@ -16,7 +16,7 @@ env: CLARINET_DISABLE_HINTS: 1 jobs: - test-contracts: + test-contracts-legacy: runs-on: ubuntu-latest steps: - name: "Checkout code" From 49a789728c54541d65480682f47131981ad6e488 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 12 Jun 2024 13:05:38 -0700 Subject: [PATCH 57/65] fix: add additional users, mint supply over 5B to simulate current --- .../proposals/test-ccip022-treasury-redemption-nyc-003.clar | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-003.clar b/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-003.clar index c840075..f572e9f 100644 --- a/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-003.clar +++ b/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-003.clar @@ -13,7 +13,9 @@ (try! (contract-call? .test-ccext-governance-token-nyc mint u1000000000000 'ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG)) ;; 1M NYC (try! (contract-call? .test-ccext-governance-token-nyc mint u5000000000000 'ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC)) ;; 5M NYC (try! (contract-call? .test-ccext-governance-token-nyc mint u10000000000000 'ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND)) ;; 10M NYC + (try! (contract-call? .test-ccext-governance-token-nyc mint u100000000000000 'ST2REHHS5J3CERCRBEPMGH7921Q6PYKAADT7JP2VB)) ;; 100M NYC + (try! (contract-call? .test-ccext-governance-token-nyc mint u1000000000000000 'ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0)) ;; 1B NYC + (try! (contract-call? .test-ccext-governance-token-nyc mint u4000000000000000 'ST3PF13W7Z0RRM42A8VZRVFQ75SV1K26RXEP8YGKJ)) ;; 4B NYC (ok true) ) ) - From 325fc86c5839e61f1e29985afe5aaacd48c883b8 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 12 Jun 2024 13:10:39 -0700 Subject: [PATCH 58/65] fix: update test to iterate and check contract, user, and ratios This expands the final test to check and log the data before and after key events in the redemption process, as well as check the redemption ratio both at the contract level and with the redemption for each user. Console logging is left on for now to help others in review. --- .../extensions/ccd012-redemption-nyc.test.ts | 275 ++++++++++++------ 1 file changed, 188 insertions(+), 87 deletions(-) diff --git a/tests/extensions/ccd012-redemption-nyc.test.ts b/tests/extensions/ccd012-redemption-nyc.test.ts index 6ef9030..ac6fd39 100644 --- a/tests/extensions/ccd012-redemption-nyc.test.ts +++ b/tests/extensions/ccd012-redemption-nyc.test.ts @@ -1,7 +1,7 @@ import { CCD007CityStacking } from "../../models/extensions/ccd007-citycoin-stacking.model.ts"; import { CCD012RedemptionNyc } from "../../models/extensions/ccd012-redemption-nyc.model.ts"; import { CCIP022TreasuryRedemptionNYC } from "../../models/proposals/ccip022-treasury-redemption-nyc.model.ts"; -import { EXTENSIONS, PROPOSALS, constructAndPassProposal, nyc, parseClarityTuple, passProposal } from "../../utils/common.ts"; +import { PROPOSALS, constructAndPassProposal, nyc, parseClarityTuple, passProposal } from "../../utils/common.ts"; import { Account, Clarinet, Chain, assertEquals, assertAlmostEquals } from "../../utils/deps.ts"; // used for asset identifier in detecting burn events @@ -134,7 +134,9 @@ Clarinet.test({ // verified by the values: 15000000000000 / 32020000000000 = 0.46845721 // console.log(expectedEvent); // console.log(executeBlock.receipts[2].events[3].contract_event.value); - executeBlock.receipts[2].events.expectPrintEvent(EXTENSIONS.CCD012_REDEMPTION_NYC, expectedEvent); + + // TEMPORARY RE-ENABLE THIS AFTER A TEST COMPARISON IN CONSOLE + // executeBlock.receipts[2].events.expectPrintEvent(EXTENSIONS.CCD012_REDEMPTION_NYC, expectedEvent); }, }); @@ -536,15 +538,41 @@ Clarinet.test({ }, }); +type RedemptionInfo = { + blockHeight: number; + contractBalance: number; + currentContractBalance: number; + redemptionRatio: number; + redemptionsEnabled: boolean; + totalRedeemed: number; + totalSupply: number; +}; + +type UserInfo = { + address: string; + nycBalances: { + address: string; + balanceV1: number; + balanceV2: number; + totalBalance: number; + }; + redemptionAmount: number; + redemptionClaims: number; +}; + Clarinet.test({ name: "ccd012-redemption-nyc: redeem-nyc() succeeds with additional claims after unstacking tokens", async 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 user4 = accounts.get("wallet_4")!; + const user1 = accounts.get("wallet_1")!; // 10k NYC + const user2 = accounts.get("wallet_2")!; // 1M NYC + const user3 = accounts.get("wallet_3")!; // 5M NYC + const user4 = accounts.get("wallet_4")!; // 10M NYC + const user5 = accounts.get("wallet_5")!; // 100M NYC + const user6 = accounts.get("wallet_6")!; // 1B NYC + const user7 = accounts.get("wallet_7")!; // 4B NYC + const users = [user1, user2, user3, user4, user5, user6, user7]; const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); const ccd012RedemptionNyc = new CCD012RedemptionNyc(chain, sender); const ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(chain, sender); @@ -552,6 +580,10 @@ Clarinet.test({ const amountStacked = 10000; const lockPeriod = 10; + const redemptionDecimals = 8; + const redemptionScaleFactor = 10 ** redemptionDecimals; + const redemptionTolerance = 1e-4; + // progress the chain to avoid underflow in // stacking reward cycle calculation chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); @@ -566,6 +598,7 @@ Clarinet.test({ } } + // fund accounts with V1 and V2 NYC const fundV1Block = passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_002); const fundV2Block = passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_003); for (let i = 0; i < fundV1Block.receipts.length; i++) { @@ -599,46 +632,87 @@ Clarinet.test({ } // get contract redemption info - const redemptionInfo = await ccd012RedemptionNyc.getRedemptionInfo().result; - // console.log("multi redemptionInfo", parseClarityTuple(redemptionInfo)); + const redemptionInfo: RedemptionInfo = await ccd012RedemptionNyc.getRedemptionInfo().result; + const redemptionInfoObject = parseClarityTuple(redemptionInfo); + console.log("------------------------------"); + console.log("contract redemption info after ccip-022 execution:"); + console.log(redemptionInfoObject); - // get user balances - const user1Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user1.address).result; - const user2Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user2.address).result; - const user3Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user3.address).result; - const user4Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user4.address).result; + // calculate the redemption ratios for comparison + const redemptionRatioInContract = redemptionInfoObject.redemptionRatio / redemptionScaleFactor; + const redemptionRatioInTest = redemptionInfoObject.contractBalance / redemptionInfoObject.totalSupply; + console.log("------------------------------"); + console.log("contract redemption ratio after ccip-022 execution:"); + console.log("ratio in contract: ", redemptionRatioInContract); + console.log("ratio calc in test: ", redemptionRatioInTest); - const user1InfoObject = parseClarityTuple(user1Info); - const user2InfoObject = parseClarityTuple(user2Info); - const user3InfoObject = parseClarityTuple(user3Info); - const user4InfoObject = parseClarityTuple(user4Info); + // check that the ratio is correctly set based on known balance and total supply + assertAlmostEquals(redemptionRatioInContract, redemptionRatioInTest, redemptionTolerance); - const userInfoObjects = [user1InfoObject, user2InfoObject, user3InfoObject, user4InfoObject]; + // check that the contract balance is equal to first known balance minus redeemed amount by all users + assertEquals(Number(redemptionInfoObject.currentContractBalance), redemptionInfoObject.contractBalance - redemptionInfoObject.totalRedeemed); - // redeem token balances once for each user + // get user balances from users array + const userInfoObjects: UserInfo[] = []; + for (let i = 0; i < users.length; i++) { + const userInfo = await ccd012RedemptionNyc.getUserRedemptionInfo(users[i].address).result; + const userInfoObject = parseClarityTuple(userInfo); + userInfoObjects.push(userInfoObject); + } + console.log("------------------------------"); + console.log("user redemption info after ccip-022 execution:"); + userInfoObjects.map((userInfo, idx) => { + console.log("user " + (idx + 1) + " info: ", userInfo); + }); + + console.log("------------------------------"); + console.log("user redemption ratios before first redemption:"); + const redemptionRatios = userInfoObjects.map((userInfo) => { + if (userInfo.nycBalances.totalBalance > 0) { + return userInfo.redemptionAmount / userInfo.nycBalances.totalBalance; + } + return 0; + }); + for (let i = 0; i < redemptionRatios.length; i++) { + console.log("redemption ratio user " + (i + 1) + ": ", redemptionRatios[i]); + assertAlmostEquals(redemptionRatios[i], redemptionRatioInContract, redemptionTolerance); + } + + // redeem token balances once for users 1-4 + // leave users 5-7 untouched with a large balance const firstRedeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender), ccd012RedemptionNyc.redeemNyc(user1), ccd012RedemptionNyc.redeemNyc(user2), ccd012RedemptionNyc.redeemNyc(user3), ccd012RedemptionNyc.redeemNyc(user4)]); - // console.log("firstRedeemBlock", firstRedeemBlock); - assertEquals(firstRedeemBlock.receipts.length, 5); + console.log("------------------------------"); + console.log("firstRedeemBlock", firstRedeemBlock); + assertEquals(firstRedeemBlock.receipts.length, userInfoObjects.length - 2); + for (let i = 0; i < firstRedeemBlock.receipts.length; i++) { + // expect first claim to fail with no NYC balance if (i === 0) { firstRedeemBlock.receipts[i].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_BALANCE_NOT_FOUND); } else { + // claim should succeed for all other users firstRedeemBlock.receipts[i].result .expectOk() .expectSome() .expectUint(userInfoObjects[i - 1].redemptionAmount); - const expectedBurnEventV1 = { - asset_identifier: NYC_V1_TOKEN, - sender: userInfoObjects[i - 1].address, - amount: userInfoObjects[i - 1].nycBalances.balanceV1, - }; - const expectedBurnEventV2 = { - asset_identifier: NYC_V2_TOKEN, - sender: userInfoObjects[i - 1].address, - amount: userInfoObjects[i - 1].nycBalances.balanceV2, - }; - firstRedeemBlock.receipts[i].events.expectFungibleTokenBurnEvent(expectedBurnEventV1.amount, expectedBurnEventV1.sender, expectedBurnEventV1.asset_identifier); - firstRedeemBlock.receipts[i].events.expectFungibleTokenBurnEvent(expectedBurnEventV2.amount, expectedBurnEventV2.sender, expectedBurnEventV2.asset_identifier); + // if there was a v1 balance, we should see a v1 burn event + if (userInfoObjects[i - 1].nycBalances.balanceV1 > 0) { + const expectedBurnEventV1 = { + asset_identifier: NYC_V1_TOKEN, + sender: userInfoObjects[i - 1].address, + amount: userInfoObjects[i - 1].nycBalances.balanceV1, + }; + firstRedeemBlock.receipts[i].events.expectFungibleTokenBurnEvent(expectedBurnEventV1.amount, expectedBurnEventV1.sender, expectedBurnEventV1.asset_identifier); + } + // if there was a v2 balance, we should see a v2 burn event + if (userInfoObjects[i - 1].nycBalances.balanceV2 > 0) { + const expectedBurnEventV2 = { + asset_identifier: NYC_V2_TOKEN, + sender: userInfoObjects[i - 1].address, + amount: userInfoObjects[i - 1].nycBalances.balanceV2, + }; + firstRedeemBlock.receipts[i].events.expectFungibleTokenBurnEvent(expectedBurnEventV2.amount, expectedBurnEventV2.sender, expectedBurnEventV2.asset_identifier); + } } } @@ -651,76 +725,103 @@ Clarinet.test({ stackingClaimBlock.receipts[i].result.expectOk().expectBool(true); } - // get user balances - const user1Info2 = await ccd012RedemptionNyc.getUserRedemptionInfo(user1.address).result; - const user2Info2 = await ccd012RedemptionNyc.getUserRedemptionInfo(user2.address).result; - const user3Info2 = await ccd012RedemptionNyc.getUserRedemptionInfo(user3.address).result; - const user4Info2 = await ccd012RedemptionNyc.getUserRedemptionInfo(user4.address).result; - - const user1InfoObject2 = parseClarityTuple(user1Info2); - const user2InfoObject2 = parseClarityTuple(user2Info2); - const user3InfoObject2 = parseClarityTuple(user3Info2); - const user4InfoObject2 = parseClarityTuple(user4Info2); - - const userInfoObjects2 = [user1InfoObject2, user2InfoObject2, user3InfoObject2, user4InfoObject2]; + // get contract redemption info + const redemptionInfo2 = await ccd012RedemptionNyc.getRedemptionInfo().result; + const redemptionInfoObject2 = parseClarityTuple(redemptionInfo2); + console.log("------------------------------"); + console.log("contract redemption info after first redemption:"); + console.log(redemptionInfoObject2); + + // check that the contract balance is equal to first known balance minus redeemed amount by all users + assertEquals(Number(redemptionInfoObject2.currentContractBalance), redemptionInfoObject2.contractBalance - redemptionInfoObject2.totalRedeemed); + + // get user balances from users array + const userInfoObjects2: UserInfo[] = []; + for (let i = 0; i < users.length; i++) { + const userInfo = await ccd012RedemptionNyc.getUserRedemptionInfo(users[i].address).result; + const userInfoObject = parseClarityTuple(userInfo); + userInfoObjects2.push(userInfoObject); + } + console.log("------------------------------"); + console.log("user redemption info after first redemption:"); + userInfoObjects2.map((userInfo, idx) => { + console.log("user " + (idx + 1) + " info: ", userInfo); + }); + + console.log("------------------------------"); + console.log("redemption ratios before second redemption:"); + const redemptionRatios2 = userInfoObjects2.map((userInfo) => { + if (userInfo.nycBalances.totalBalance > 0) { + return userInfo.redemptionAmount / userInfo.nycBalances.totalBalance; + } + return 0; + }); + for (let i = 0; i < redemptionRatios2.length; i++) { + console.log("redemption ratio user " + (i + 1) + ": ", redemptionRatios2[i]); + assertAlmostEquals(redemptionRatios2[i], redemptionRatioInContract, redemptionTolerance); + } // act // redeem token balances once for each user - const secondRedeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender), ccd012RedemptionNyc.redeemNyc(user1), ccd012RedemptionNyc.redeemNyc(user2), ccd012RedemptionNyc.redeemNyc(user3), ccd012RedemptionNyc.redeemNyc(user4)]); - // console.log("secondRedeemBlock", secondRedeemBlock); + const secondRedeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender), ccd012RedemptionNyc.redeemNyc(user1), ccd012RedemptionNyc.redeemNyc(user2), ccd012RedemptionNyc.redeemNyc(user3), ccd012RedemptionNyc.redeemNyc(user4), ccd012RedemptionNyc.redeemNyc(user5), ccd012RedemptionNyc.redeemNyc(user6), ccd012RedemptionNyc.redeemNyc(user7)]); + console.log("------------------------------"); + console.log("secondRedeemBlock", secondRedeemBlock); + assertEquals(secondRedeemBlock.receipts.length, userInfoObjects2.length + 1); // assert - assertEquals(secondRedeemBlock.receipts.length, 5); for (let i = 0; i < secondRedeemBlock.receipts.length; i++) { + // expect first claim to fail with no NYC balance if (i === 0) { secondRedeemBlock.receipts[i].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_BALANCE_NOT_FOUND); } else { + // claim should succeed for all other users secondRedeemBlock.receipts[i].result .expectOk() .expectSome() .expectUint(userInfoObjects2[i - 1].redemptionAmount); - const expectedBurnEventV1 = { - asset_identifier: NYC_V1_TOKEN, - sender: userInfoObjects[i - 1].address, - amount: userInfoObjects[i - 1].nycBalances.balanceV1, - }; - const expectedBurnEventV2 = { - asset_identifier: NYC_V2_TOKEN, - sender: userInfoObjects[i - 1].address, - amount: userInfoObjects[i - 1].nycBalances.balanceV2, - }; - firstRedeemBlock.receipts[i].events.expectFungibleTokenBurnEvent(expectedBurnEventV1.amount, expectedBurnEventV1.sender, expectedBurnEventV1.asset_identifier); - firstRedeemBlock.receipts[i].events.expectFungibleTokenBurnEvent(expectedBurnEventV2.amount, expectedBurnEventV2.sender, expectedBurnEventV2.asset_identifier); + // if there was a v1 balance, we should see a v1 burn event + if (userInfoObjects2[i - 1].nycBalances.balanceV1 > 0) { + const expectedBurnEventV1 = { + asset_identifier: NYC_V1_TOKEN, + sender: userInfoObjects2[i - 1].address, + amount: userInfoObjects2[i - 1].nycBalances.balanceV1, + }; + secondRedeemBlock.receipts[i].events.expectFungibleTokenBurnEvent(expectedBurnEventV1.amount, expectedBurnEventV1.sender, expectedBurnEventV1.asset_identifier); + } + // if there was a v2 balance, we should see a v2 burn event + if (userInfoObjects2[i - 1].nycBalances.balanceV2 > 0) { + const expectedBurnEventV2 = { + asset_identifier: NYC_V2_TOKEN, + sender: userInfoObjects2[i - 1].address, + amount: userInfoObjects2[i - 1].nycBalances.balanceV2, + }; + secondRedeemBlock.receipts[i].events.expectFungibleTokenBurnEvent(expectedBurnEventV2.amount, expectedBurnEventV2.sender, expectedBurnEventV2.asset_identifier); + } } } // get the redemption info from the contract for analysis - const redemptionInfoResult = parseClarityTuple(await ccd012RedemptionNyc.getRedemptionInfo().result); - - // console.log("------------------------------"); - // console.log("get redemption info"); - // console.log(redemptionInfoResult); - - // calculate the redemption ratios for comparison - const redemptionDecimals = 8; - const redemptionScaleFactor = 10 ** redemptionDecimals; - const redemptionRatio1 = redemptionInfoResult.redemptionRatio / redemptionScaleFactor; - const redemptionRatio2 = redemptionInfoResult.contractBalance / redemptionInfoResult.totalSupply; - - // console.log("redemption ratio 1: ", redemptionRatio1); - // console.log("redemption ratio 2: ", redemptionRatio2); - - // check that the ratio is correctly set based on known balance and total supply - assertAlmostEquals(redemptionRatio1, redemptionRatio2, redemptionDecimals); - - // check that the balance is equal to first known balance minus redeemed amount - assertEquals(Number(redemptionInfoResult.currentContractBalance), redemptionInfoResult.contractBalance - redemptionInfoResult.totalRedeemed); - - // console.log("----------"); - // console.log("user1Info", user1InfoObject2); - // console.log("user2Info", user2InfoObject2); - // console.log("user3Info", user3InfoObject2); - // console.log("user4Info", user4InfoObject2); - // console.log("----------"); + const redemptionInfo3 = await ccd012RedemptionNyc.getRedemptionInfo().result; + const redemptionInfoObject3 = parseClarityTuple(redemptionInfo3); + + console.log("------------------------------"); + console.log("contract redemption info after second redemption:"); + console.log(redemptionInfoObject3); + + // check that the contract balance is equal to first known balance minus redeemed amount by all users + assertEquals(Number(redemptionInfoObject3.currentContractBalance), redemptionInfoObject3.contractBalance - redemptionInfoObject3.totalRedeemed); + + // get user balances from users array + const userInfoObjects3: UserInfo[] = []; + for (let i = 0; i < users.length; i++) { + const userInfo = await ccd012RedemptionNyc.getUserRedemptionInfo(users[i].address).result; + const userInfoObject = parseClarityTuple(userInfo); + userInfoObjects3.push(userInfoObject); + } + console.log("------------------------------"); + console.log("user redemption info after second redemption:"); + userInfoObjects3.map((userInfo, idx) => { + console.log("user " + (idx + 1) + " info: ", userInfo); + }); }, }); From 04358b5fac83c5aaa21facc54258a21865ed927b Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 12 Jun 2024 13:15:46 -0700 Subject: [PATCH 59/65] fix: update expected print event format and related test --- tests/extensions/ccd012-redemption-nyc.test.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/extensions/ccd012-redemption-nyc.test.ts b/tests/extensions/ccd012-redemption-nyc.test.ts index ac6fd39..809ad06 100644 --- a/tests/extensions/ccd012-redemption-nyc.test.ts +++ b/tests/extensions/ccd012-redemption-nyc.test.ts @@ -1,7 +1,7 @@ import { CCD007CityStacking } from "../../models/extensions/ccd007-citycoin-stacking.model.ts"; import { CCD012RedemptionNyc } from "../../models/extensions/ccd012-redemption-nyc.model.ts"; import { CCIP022TreasuryRedemptionNYC } from "../../models/proposals/ccip022-treasury-redemption-nyc.model.ts"; -import { PROPOSALS, constructAndPassProposal, nyc, parseClarityTuple, passProposal } from "../../utils/common.ts"; +import { EXTENSIONS, PROPOSALS, constructAndPassProposal, nyc, parseClarityTuple, passProposal } from "../../utils/common.ts"; import { Account, Clarinet, Chain, assertEquals, assertAlmostEquals } from "../../utils/deps.ts"; // used for asset identifier in detecting burn events @@ -129,14 +129,13 @@ Clarinet.test({ executeBlock.receipts[i].result.expectOk().expectUint(i + 1); } - const expectedEvent = `{notification: "intialize-contract", payload: {blockHeight: u12611, contractBalance: u15000000000000, currentContractBalance: u15000000000000, redemptionRatio: u46845721, redemptionsEnabled: true, totalRedeemed: u0, totalSupply: u32020000000000}}`; + const expectedEvent = `{notification: "intialize-contract", payload: {blockHeight: u12611, contractBalance: u15000000000000, currentContractBalance: u15000000000000, redemptionRatio: u292282, redemptionsEnabled: true, totalRedeemed: u0, totalSupply: u5132020000000000}}`; // redemption ratio obtained through console logging below // verified by the values: 15000000000000 / 32020000000000 = 0.46845721 // console.log(expectedEvent); // console.log(executeBlock.receipts[2].events[3].contract_event.value); - // TEMPORARY RE-ENABLE THIS AFTER A TEST COMPARISON IN CONSOLE - // executeBlock.receipts[2].events.expectPrintEvent(EXTENSIONS.CCD012_REDEMPTION_NYC, expectedEvent); + executeBlock.receipts[2].events.expectPrintEvent(EXTENSIONS.CCD012_REDEMPTION_NYC, expectedEvent); }, }); From db24f99ca193689b47264e80631c9364409771f5 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 12 Jun 2024 13:26:58 -0700 Subject: [PATCH 60/65] fix: add missing test for trying to enable extension twice --- .../extensions/ccd012-redemption-nyc.test.ts | 66 ++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/tests/extensions/ccd012-redemption-nyc.test.ts b/tests/extensions/ccd012-redemption-nyc.test.ts index 809ad06..303f9ee 100644 --- a/tests/extensions/ccd012-redemption-nyc.test.ts +++ b/tests/extensions/ccd012-redemption-nyc.test.ts @@ -71,7 +71,70 @@ Clarinet.test({ Clarinet.test({ name: "ccd012-redemption-nyc: initialize-redemption() fails with ERR_ALREADY_ENABLED if called more than once", async fn(chain: Chain, accounts: Map) { - 0; + // 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 ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(chain, sender); + + // set stacking parameters + const amountStacked = 500; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + + // initialize contracts + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + + // mint and move funds + passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_003); + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked, lockPeriod)]); + // for length of mineBlock array, expectOk and expectBool(true) + for (let i = 0; i < stackingBlock.receipts.length; i++) { + stackingBlock.receipts[i].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 two yes votes, one no vote + const votingBlock = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user2, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user3, false)]); + for (let i = 0; i < votingBlock.receipts.length; i++) { + votingBlock.receipts[i].result.expectOk().expectBool(true); + } + + const executeBlock = passProposal(chain, accounts, PROPOSALS.CCIP_022); + + assertEquals(executeBlock.receipts.length, 3); + for (let i = 0; i < executeBlock.receipts.length; i++) { + executeBlock.receipts[i].result.expectOk().expectUint(i + 1); + } + + const expectedEvent = `{notification: "intialize-contract", payload: {blockHeight: u12611, contractBalance: u15000000000000, currentContractBalance: u15000000000000, redemptionRatio: u292282, redemptionsEnabled: true, totalRedeemed: u0, totalSupply: u5132020000000000}}`; + // redemption ratio obtained through console logging below + // console.log(expectedEvent); + // console.log(executeBlock.receipts[2].events[3].contract_event.value); + + executeBlock.receipts[2].events.expectPrintEvent(EXTENSIONS.CCD012_REDEMPTION_NYC, expectedEvent); + + // act + const secondInit = passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_004); + + // assert + assertEquals(secondInit.receipts.length, 3); + secondInit.receipts[0].result.expectOk().expectUint(1); + secondInit.receipts[1].result.expectOk().expectUint(2); + secondInit.receipts[2].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_ALREADY_ENABLED); }, }); @@ -131,7 +194,6 @@ Clarinet.test({ const expectedEvent = `{notification: "intialize-contract", payload: {blockHeight: u12611, contractBalance: u15000000000000, currentContractBalance: u15000000000000, redemptionRatio: u292282, redemptionsEnabled: true, totalRedeemed: u0, totalSupply: u5132020000000000}}`; // redemption ratio obtained through console logging below - // verified by the values: 15000000000000 / 32020000000000 = 0.46845721 // console.log(expectedEvent); // console.log(executeBlock.receipts[2].events[3].contract_event.value); From 574af4a0c790a49e84d7025502e9ac543c3e6d6a Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 19 Jun 2024 10:26:46 -0700 Subject: [PATCH 61/65] fix: support edge case of claim being greater than contract balance This was identified in peer review, since mining claims are still available there is a slight chance that the amount claimed by the final claimant may be greater than the amount of STX in the contract. As a result, the user would be required to reduce their NYC balance to be able to claim the remaining STX in the contract, which is now mitigated by the added IF statement. --- .../extensions/ccd012-redemption-nyc.clar | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/contracts/extensions/ccd012-redemption-nyc.clar b/contracts/extensions/ccd012-redemption-nyc.clar index 1df5ebe..07b1b7a 100644 --- a/contracts/extensions/ccd012-redemption-nyc.clar +++ b/contracts/extensions/ccd012-redemption-nyc.clar @@ -104,6 +104,7 @@ (balanceV1 (unwrap! (contract-call? .test-ccext-governance-token-nyc-v1 get-balance userAddress) ERR_BALANCE_NOT_FOUND)) (balanceV2 (unwrap! (contract-call? .test-ccext-governance-token-nyc get-balance userAddress) ERR_BALANCE_NOT_FOUND)) (totalBalance (+ (* balanceV1 MICRO_CITYCOINS) balanceV2)) + (contractCurrentBalance (get-redemption-contract-current-balance)) (redemptionAmount (get-redemption-for-balance totalBalance)) (redemptionClaims (default-to u0 (get-redemption-amount-claimed userAddress))) ) @@ -111,6 +112,8 @@ (asserts! (var-get redemptionsEnabled) ERR_NOT_ENABLED) ;; check that user has at least one positive balance (asserts! (> (+ balanceV1 balanceV2) u0) ERR_BALANCE_NOT_FOUND) ;; cheaper, credit: LNow + ;; check that contract has a positive balance + (asserts! (> contractCurrentBalance u0) ERR_NOTHING_TO_REDEEM) ;; check that redemption amount is > 0 (asserts! (and (is-some redemptionAmount) (> (unwrap-panic redemptionAmount) u0)) ERR_NOTHING_TO_REDEEM) ;; burn NYC @@ -118,11 +121,21 @@ ;; MAINNET: SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 (and (> balanceV1 u0) (try! (contract-call? .test-ccext-governance-token-nyc-v1 burn balanceV1 userAddress))) (and (> balanceV2 u0) (try! (contract-call? .test-ccext-governance-token-nyc burn balanceV2 userAddress))) - ;; transfer STX - (try! (as-contract (stx-transfer? (unwrap-panic redemptionAmount) tx-sender userAddress))) - ;; update redemption claims - (var-set totalRedeemed (+ (var-get totalRedeemed) (unwrap-panic redemptionAmount))) - (map-set RedemptionClaims userAddress (+ redemptionClaims (unwrap-panic redemptionAmount))) + ;; handle redemption transfer + (if (< (unwrap-panic redemptionAmount) contractCurrentBalance) + ;; if the redemption amount is less than the contract balance, redeem the amount + (begin + (try! (as-contract (stx-transfer? (unwrap-panic redemptionAmount) SELF userAddress))) + (var-set totalRedeemed (+ (var-get totalRedeemed) (unwrap-panic redemptionAmount))) + (map-set RedemptionClaims userAddress (+ redemptionClaims (unwrap-panic redemptionAmount))) + ) + ;; else if redemption amount is greater than contract balance, redeem the remaining balance + (begin + (try! (as-contract (stx-transfer? contractCurrentBalance SELF userAddress))) + (var-set totalRedeemed (+ (var-get totalRedeemed) contractCurrentBalance)) + (map-set RedemptionClaims userAddress (+ redemptionClaims contractCurrentBalance)) + ) + ) ;; print redemption info (print { notification: "contract-redemption", From 503f4d214eee87ea67a01f447083dd163c855842 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 19 Jun 2024 11:27:05 -0700 Subject: [PATCH 62/65] fix: move calculation for edge case to get-redemption-for-balance --- .../extensions/ccd012-redemption-nyc.clar | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/contracts/extensions/ccd012-redemption-nyc.clar b/contracts/extensions/ccd012-redemption-nyc.clar index 07b1b7a..2e5abd6 100644 --- a/contracts/extensions/ccd012-redemption-nyc.clar +++ b/contracts/extensions/ccd012-redemption-nyc.clar @@ -104,16 +104,15 @@ (balanceV1 (unwrap! (contract-call? .test-ccext-governance-token-nyc-v1 get-balance userAddress) ERR_BALANCE_NOT_FOUND)) (balanceV2 (unwrap! (contract-call? .test-ccext-governance-token-nyc get-balance userAddress) ERR_BALANCE_NOT_FOUND)) (totalBalance (+ (* balanceV1 MICRO_CITYCOINS) balanceV2)) - (contractCurrentBalance (get-redemption-contract-current-balance)) (redemptionAmount (get-redemption-for-balance totalBalance)) - (redemptionClaims (default-to u0 (get-redemption-amount-claimed userAddress))) + (redemptionClaimed (default-to u0 (get-redemption-amount-claimed userAddress))) ) ;; check if redemptions are enabled (asserts! (var-get redemptionsEnabled) ERR_NOT_ENABLED) ;; check that user has at least one positive balance (asserts! (> (+ balanceV1 balanceV2) u0) ERR_BALANCE_NOT_FOUND) ;; cheaper, credit: LNow ;; check that contract has a positive balance - (asserts! (> contractCurrentBalance u0) ERR_NOTHING_TO_REDEEM) + (asserts! (> (get-redemption-contract-current-balance) u0) ERR_NOTHING_TO_REDEEM) ;; check that redemption amount is > 0 (asserts! (and (is-some redemptionAmount) (> (unwrap-panic redemptionAmount) u0)) ERR_NOTHING_TO_REDEEM) ;; burn NYC @@ -121,21 +120,11 @@ ;; MAINNET: SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 (and (> balanceV1 u0) (try! (contract-call? .test-ccext-governance-token-nyc-v1 burn balanceV1 userAddress))) (and (> balanceV2 u0) (try! (contract-call? .test-ccext-governance-token-nyc burn balanceV2 userAddress))) - ;; handle redemption transfer - (if (< (unwrap-panic redemptionAmount) contractCurrentBalance) - ;; if the redemption amount is less than the contract balance, redeem the amount - (begin - (try! (as-contract (stx-transfer? (unwrap-panic redemptionAmount) SELF userAddress))) - (var-set totalRedeemed (+ (var-get totalRedeemed) (unwrap-panic redemptionAmount))) - (map-set RedemptionClaims userAddress (+ redemptionClaims (unwrap-panic redemptionAmount))) - ) - ;; else if redemption amount is greater than contract balance, redeem the remaining balance - (begin - (try! (as-contract (stx-transfer? contractCurrentBalance SELF userAddress))) - (var-set totalRedeemed (+ (var-get totalRedeemed) contractCurrentBalance)) - (map-set RedemptionClaims userAddress (+ redemptionClaims contractCurrentBalance)) - ) - ) + ;; transfer STX + (try! (as-contract (stx-transfer? (unwrap-panic redemptionAmount) SELF userAddress))) + ;; update redemption claims + (var-set totalRedeemed (+ (var-get totalRedeemed) (unwrap-panic redemptionAmount))) + (map-set RedemptionClaims userAddress (+ redemptionClaimed (unwrap-panic redemptionAmount))) ;; print redemption info (print { notification: "contract-redemption", @@ -217,9 +206,13 @@ ( (redemptionAmountScaled (* (var-get redemptionRatio) balance)) (redemptionAmount (/ redemptionAmountScaled REDEMPTION_SCALE_FACTOR)) + (contractCurrentBalance (get-redemption-contract-current-balance)) ) (if (> redemptionAmount u0) - (some redemptionAmount) + (if (< redemptionAmount contractCurrentBalance) + (some redemptionAmount) + (some contractCurrentBalance) + ) none ) ) From 534d55f6eb401bdb498ea00ab6dc5b46cb4779c1 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 19 Jun 2024 11:31:35 -0700 Subject: [PATCH 63/65] fix: add comments to explain balance calculation --- contracts/extensions/ccd012-redemption-nyc.clar | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/extensions/ccd012-redemption-nyc.clar b/contracts/extensions/ccd012-redemption-nyc.clar index 2e5abd6..9ad3b8c 100644 --- a/contracts/extensions/ccd012-redemption-nyc.clar +++ b/contracts/extensions/ccd012-redemption-nyc.clar @@ -210,9 +210,12 @@ ) (if (> redemptionAmount u0) (if (< redemptionAmount contractCurrentBalance) + ;; if redemption amount is less than contract balance, return redemption amount (some redemptionAmount) + ;; if redemption amount is greater than contract balance, return contract balance (some contractCurrentBalance) ) + ;; if redemption amount is 0, return none none ) ) From 4122d743b2ff0fa8d7f7b898e0050a6fb22e176d Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 19 Jun 2024 11:38:08 -0700 Subject: [PATCH 64/65] chore: clean up test console logging --- .../extensions/ccd012-redemption-nyc.test.ts | 76 +++++++++---------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/tests/extensions/ccd012-redemption-nyc.test.ts b/tests/extensions/ccd012-redemption-nyc.test.ts index 303f9ee..eeb3ad1 100644 --- a/tests/extensions/ccd012-redemption-nyc.test.ts +++ b/tests/extensions/ccd012-redemption-nyc.test.ts @@ -695,17 +695,17 @@ Clarinet.test({ // get contract redemption info const redemptionInfo: RedemptionInfo = await ccd012RedemptionNyc.getRedemptionInfo().result; const redemptionInfoObject = parseClarityTuple(redemptionInfo); - console.log("------------------------------"); - console.log("contract redemption info after ccip-022 execution:"); - console.log(redemptionInfoObject); + // console.log("------------------------------"); + // console.log("contract redemption info after ccip-022 execution:"); + // console.log(redemptionInfoObject); // calculate the redemption ratios for comparison const redemptionRatioInContract = redemptionInfoObject.redemptionRatio / redemptionScaleFactor; const redemptionRatioInTest = redemptionInfoObject.contractBalance / redemptionInfoObject.totalSupply; - console.log("------------------------------"); - console.log("contract redemption ratio after ccip-022 execution:"); - console.log("ratio in contract: ", redemptionRatioInContract); - console.log("ratio calc in test: ", redemptionRatioInTest); + // console.log("------------------------------"); + // console.log("contract redemption ratio after ccip-022 execution:"); + // console.log("ratio in contract: ", redemptionRatioInContract); + // console.log("ratio calc in test: ", redemptionRatioInTest); // check that the ratio is correctly set based on known balance and total supply assertAlmostEquals(redemptionRatioInContract, redemptionRatioInTest, redemptionTolerance); @@ -720,14 +720,14 @@ Clarinet.test({ const userInfoObject = parseClarityTuple(userInfo); userInfoObjects.push(userInfoObject); } - console.log("------------------------------"); - console.log("user redemption info after ccip-022 execution:"); - userInfoObjects.map((userInfo, idx) => { - console.log("user " + (idx + 1) + " info: ", userInfo); - }); + // console.log("------------------------------"); + // console.log("user redemption info after ccip-022 execution:"); + // userInfoObjects.map((userInfo, idx) => { + // console.log("user " + (idx + 1) + " info: ", userInfo); + // }); - console.log("------------------------------"); - console.log("user redemption ratios before first redemption:"); + // console.log("------------------------------"); + // console.log("user redemption ratios before first redemption:"); const redemptionRatios = userInfoObjects.map((userInfo) => { if (userInfo.nycBalances.totalBalance > 0) { return userInfo.redemptionAmount / userInfo.nycBalances.totalBalance; @@ -735,15 +735,15 @@ Clarinet.test({ return 0; }); for (let i = 0; i < redemptionRatios.length; i++) { - console.log("redemption ratio user " + (i + 1) + ": ", redemptionRatios[i]); + // console.log("redemption ratio user " + (i + 1) + ": ", redemptionRatios[i]); assertAlmostEquals(redemptionRatios[i], redemptionRatioInContract, redemptionTolerance); } // redeem token balances once for users 1-4 // leave users 5-7 untouched with a large balance const firstRedeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender), ccd012RedemptionNyc.redeemNyc(user1), ccd012RedemptionNyc.redeemNyc(user2), ccd012RedemptionNyc.redeemNyc(user3), ccd012RedemptionNyc.redeemNyc(user4)]); - console.log("------------------------------"); - console.log("firstRedeemBlock", firstRedeemBlock); + // console.log("------------------------------"); + // console.log("firstRedeemBlock", firstRedeemBlock); assertEquals(firstRedeemBlock.receipts.length, userInfoObjects.length - 2); for (let i = 0; i < firstRedeemBlock.receipts.length; i++) { @@ -789,9 +789,9 @@ Clarinet.test({ // get contract redemption info const redemptionInfo2 = await ccd012RedemptionNyc.getRedemptionInfo().result; const redemptionInfoObject2 = parseClarityTuple(redemptionInfo2); - console.log("------------------------------"); - console.log("contract redemption info after first redemption:"); - console.log(redemptionInfoObject2); + // console.log("------------------------------"); + // console.log("contract redemption info after first redemption:"); + // console.log(redemptionInfoObject2); // check that the contract balance is equal to first known balance minus redeemed amount by all users assertEquals(Number(redemptionInfoObject2.currentContractBalance), redemptionInfoObject2.contractBalance - redemptionInfoObject2.totalRedeemed); @@ -803,14 +803,14 @@ Clarinet.test({ const userInfoObject = parseClarityTuple(userInfo); userInfoObjects2.push(userInfoObject); } - console.log("------------------------------"); - console.log("user redemption info after first redemption:"); - userInfoObjects2.map((userInfo, idx) => { - console.log("user " + (idx + 1) + " info: ", userInfo); - }); + // console.log("------------------------------"); + // console.log("user redemption info after first redemption:"); + // userInfoObjects2.map((userInfo, idx) => { + // console.log("user " + (idx + 1) + " info: ", userInfo); + // }); - console.log("------------------------------"); - console.log("redemption ratios before second redemption:"); + // console.log("------------------------------"); + // console.log("redemption ratios before second redemption:"); const redemptionRatios2 = userInfoObjects2.map((userInfo) => { if (userInfo.nycBalances.totalBalance > 0) { return userInfo.redemptionAmount / userInfo.nycBalances.totalBalance; @@ -818,15 +818,15 @@ Clarinet.test({ return 0; }); for (let i = 0; i < redemptionRatios2.length; i++) { - console.log("redemption ratio user " + (i + 1) + ": ", redemptionRatios2[i]); + // console.log("redemption ratio user " + (i + 1) + ": ", redemptionRatios2[i]); assertAlmostEquals(redemptionRatios2[i], redemptionRatioInContract, redemptionTolerance); } // act // redeem token balances once for each user const secondRedeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender), ccd012RedemptionNyc.redeemNyc(user1), ccd012RedemptionNyc.redeemNyc(user2), ccd012RedemptionNyc.redeemNyc(user3), ccd012RedemptionNyc.redeemNyc(user4), ccd012RedemptionNyc.redeemNyc(user5), ccd012RedemptionNyc.redeemNyc(user6), ccd012RedemptionNyc.redeemNyc(user7)]); - console.log("------------------------------"); - console.log("secondRedeemBlock", secondRedeemBlock); + // console.log("------------------------------"); + // console.log("secondRedeemBlock", secondRedeemBlock); assertEquals(secondRedeemBlock.receipts.length, userInfoObjects2.length + 1); // assert @@ -865,9 +865,9 @@ Clarinet.test({ const redemptionInfo3 = await ccd012RedemptionNyc.getRedemptionInfo().result; const redemptionInfoObject3 = parseClarityTuple(redemptionInfo3); - console.log("------------------------------"); - console.log("contract redemption info after second redemption:"); - console.log(redemptionInfoObject3); + // console.log("------------------------------"); + // console.log("contract redemption info after second redemption:"); + // console.log(redemptionInfoObject3); // check that the contract balance is equal to first known balance minus redeemed amount by all users assertEquals(Number(redemptionInfoObject3.currentContractBalance), redemptionInfoObject3.contractBalance - redemptionInfoObject3.totalRedeemed); @@ -879,10 +879,10 @@ Clarinet.test({ const userInfoObject = parseClarityTuple(userInfo); userInfoObjects3.push(userInfoObject); } - console.log("------------------------------"); - console.log("user redemption info after second redemption:"); - userInfoObjects3.map((userInfo, idx) => { - console.log("user " + (idx + 1) + " info: ", userInfo); - }); + // console.log("------------------------------"); + // console.log("user redemption info after second redemption:"); + // userInfoObjects3.map((userInfo, idx) => { + // console.log("user " + (idx + 1) + " info: ", userInfo); + // }); }, }); From a44d7c992056afcf6c46e71e147b73100dd8f877 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Wed, 19 Jun 2024 12:47:43 -0700 Subject: [PATCH 65/65] fix: add ccip-022 hash --- contracts/proposals/ccip022-treasury-redemption-nyc.clar | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/proposals/ccip022-treasury-redemption-nyc.clar b/contracts/proposals/ccip022-treasury-redemption-nyc.clar index 20abf98..405ae88 100644 --- a/contracts/proposals/ccip022-treasury-redemption-nyc.clar +++ b/contracts/proposals/ccip022-treasury-redemption-nyc.clar @@ -20,7 +20,7 @@ (define-constant CCIP_022 { name: "CityCoins Treasury Redemption (NYC)", link: "https://github.com/citycoins/governance/blob/feat/add-ccip-022/ccips/ccip-022/ccip-022-citycoins-treasury-redemption-nyc.md", - hash: "TBD", + hash: "2a22deaae6e7c12bb7726c3061a0f20515644ca9", }) (define-constant VOTE_SCALE_FACTOR (pow u10 u16)) ;; 16 decimal places