diff --git a/.github/workflows/test-contracts.yaml b/.github/workflows/test-contracts.yaml index 209728e8..884badd1 100644 --- a/.github/workflows/test-contracts.yaml +++ b/.github/workflows/test-contracts.yaml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest steps: - name: "Checkout code" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Check contract syntax" uses: docker://hirosystems/clarinet:1.8.0 with: @@ -33,6 +33,7 @@ jobs: with: args: test --coverage - name: "Upload code coverage" - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: files: ./coverage.lcov + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/Clarinet.toml b/Clarinet.toml index 42fae9f4..56ef02f8 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -164,6 +164,11 @@ 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 @@ -216,12 +221,33 @@ 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" @@ -231,6 +257,18 @@ 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" @@ -479,6 +517,11 @@ 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 diff --git a/contracts/legacy/citycoin-core-trait.clar b/contracts/legacy/citycoin-core-trait.clar new file mode 100644 index 00000000..b2945329 --- /dev/null +++ b/contracts/legacy/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/contracts/legacy/citycoin-token-trait.clar b/contracts/legacy/citycoin-token-trait.clar new file mode 100644 index 00000000..107b4e41 --- /dev/null +++ b/contracts/legacy/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/contracts/legacy/citycoin-vrf.clar b/contracts/legacy/citycoin-vrf.clar new file mode 100644 index 00000000..023c82f5 --- /dev/null +++ b/contracts/legacy/citycoin-vrf.clar @@ -0,0 +1,83 @@ +;; CITYCOIN VRF CONTRACT + +;; VRF + +;; Read the on-chain VRF and turn the lower 16 bytes into a uint, in order to sample the set of miners and determine +;; which one may claim the token batch for the given block height. +(define-read-only (get-random-uint-at-block (stacksBlock uint)) + (let ( + (vrf-lower-uint-opt + (match (get-block-info? vrf-seed stacksBlock) + vrf-seed (some (buff-to-uint-le (lower-16-le vrf-seed))) + none)) + ) + vrf-lower-uint-opt) +) + +;; UTILITIES + +;; lookup table for converting 1-byte buffers to uints via index-of +(define-constant BUFF_TO_BYTE (list + 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0a 0x0b 0x0c 0x0d 0x0e 0x0f + 0x10 0x11 0x12 0x13 0x14 0x15 0x16 0x17 0x18 0x19 0x1a 0x1b 0x1c 0x1d 0x1e 0x1f + 0x20 0x21 0x22 0x23 0x24 0x25 0x26 0x27 0x28 0x29 0x2a 0x2b 0x2c 0x2d 0x2e 0x2f + 0x30 0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38 0x39 0x3a 0x3b 0x3c 0x3d 0x3e 0x3f + 0x40 0x41 0x42 0x43 0x44 0x45 0x46 0x47 0x48 0x49 0x4a 0x4b 0x4c 0x4d 0x4e 0x4f + 0x50 0x51 0x52 0x53 0x54 0x55 0x56 0x57 0x58 0x59 0x5a 0x5b 0x5c 0x5d 0x5e 0x5f + 0x60 0x61 0x62 0x63 0x64 0x65 0x66 0x67 0x68 0x69 0x6a 0x6b 0x6c 0x6d 0x6e 0x6f + 0x70 0x71 0x72 0x73 0x74 0x75 0x76 0x77 0x78 0x79 0x7a 0x7b 0x7c 0x7d 0x7e 0x7f + 0x80 0x81 0x82 0x83 0x84 0x85 0x86 0x87 0x88 0x89 0x8a 0x8b 0x8c 0x8d 0x8e 0x8f + 0x90 0x91 0x92 0x93 0x94 0x95 0x96 0x97 0x98 0x99 0x9a 0x9b 0x9c 0x9d 0x9e 0x9f + 0xa0 0xa1 0xa2 0xa3 0xa4 0xa5 0xa6 0xa7 0xa8 0xa9 0xaa 0xab 0xac 0xad 0xae 0xaf + 0xb0 0xb1 0xb2 0xb3 0xb4 0xb5 0xb6 0xb7 0xb8 0xb9 0xba 0xbb 0xbc 0xbd 0xbe 0xbf + 0xc0 0xc1 0xc2 0xc3 0xc4 0xc5 0xc6 0xc7 0xc8 0xc9 0xca 0xcb 0xcc 0xcd 0xce 0xcf + 0xd0 0xd1 0xd2 0xd3 0xd4 0xd5 0xd6 0xd7 0xd8 0xd9 0xda 0xdb 0xdc 0xdd 0xde 0xdf + 0xe0 0xe1 0xe2 0xe3 0xe4 0xe5 0xe6 0xe7 0xe8 0xe9 0xea 0xeb 0xec 0xed 0xee 0xef + 0xf0 0xf1 0xf2 0xf3 0xf4 0xf5 0xf6 0xf7 0xf8 0xf9 0xfa 0xfb 0xfc 0xfd 0xfe 0xff +)) + +;; Convert a 1-byte buffer into its uint representation. +(define-private (buff-to-u8 (byte (buff 1))) + (unwrap-panic (index-of BUFF_TO_BYTE byte)) +) + +;; Convert a little-endian 16-byte buff into a uint. +(define-private (buff-to-uint-le (word (buff 16))) + (get acc + (fold add-and-shift-uint-le (list u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11 u12 u13 u14 u15) { acc: u0, data: word }) + ) +) + +;; Inner fold function for converting a 16-byte buff into a uint. +(define-private (add-and-shift-uint-le (idx uint) (input { acc: uint, data: (buff 16) })) + (let ( + (acc (get acc input)) + (data (get data input)) + (byte (buff-to-u8 (unwrap-panic (element-at data idx)))) + ) + { + ;; acc = byte * (2**(8 * (15 - idx))) + acc + acc: (+ (* byte (pow u2 (* u8 (- u15 idx)))) acc), + data: data + }) +) + +;; Convert the lower 16 bytes of a buff into a little-endian uint. +(define-private (lower-16-le (input (buff 32))) + (get acc + (fold lower-16-le-closure (list u16 u17 u18 u19 u20 u21 u22 u23 u24 u25 u26 u27 u28 u29 u30 u31) { acc: 0x, data: input }) + ) +) + +;; Inner closure for obtaining the lower 16 bytes of a 32-byte buff +(define-private (lower-16-le-closure (idx uint) (input { acc: (buff 16), data: (buff 32) })) + (let ( + (acc (get acc input)) + (data (get data input)) + (byte (unwrap-panic (element-at data idx))) + ) + { + acc: (unwrap-panic (as-max-len? (concat acc byte) u16)), + data: data + }) +) \ No newline at end of file diff --git a/contracts/legacy/miamicoin-auth.clar b/contracts/legacy/miamicoin-auth.clar new file mode 100644 index 00000000..7239e522 --- /dev/null +++ b/contracts/legacy/miamicoin-auth.clar @@ -0,0 +1,620 @@ +;; MIAMICOIN AUTH CONTRACT +;; CityCoins Protocol Version 1.0.0 + +(define-constant CONTRACT_OWNER tx-sender) + +;; TRAIT DEFINITIONS + +(use-trait coreTrait .citycoin-core-trait.citycoin-core) +(use-trait tokenTrait .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 'STFCVYY1RJDNJHST7RRTPACYHVJQDJ7R1DWTQHQA) +;; MAINNET +;; (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 'ST1J4G6RR643BCG8G8SR6M2D9Z9KXT2NJDRK3FBTK true) +(map-insert Approvers 'ST20ATRN26N9P05V2F1RHFRV24X8C8M3W54E427B2 true) +(map-insert Approvers 'ST21HMSJATHZ888PD0S0SSTWP4J61TCRJYEVQ0STB true) +(map-insert Approvers 'ST2QXSK64YQX3CQPC530K79XWQ98XFAM9W3XKEH3N true) +(map-insert Approvers 'ST3DG3R65C9TTEEW5BC5XTSY0M1JM7NBE7GVWKTVJ true) + +;; MAINNET +;; (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) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; TESTING FUNCTIONS +;; DELETE BEFORE DEPLOYING TO MAINNET +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +(define-constant DEPLOYED_AT block-height) + +(define-private (is-test-env) + (<= DEPLOYED_AT u5) +) + +(define-public (test-initialize-contracts (coreContract )) + (let + ( + (coreContractAddress (contract-of coreContract)) + ) + (asserts! (is-test-env) (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-public (test-set-active-core-contract) + (begin + (asserts! (is-test-env) (err ERR_UNAUTHORIZED)) + (ok (var-set activeCoreContract .miamicoin-core-v1)) + ) +) + +(define-public (test-set-city-wallet-patch (coreContract )) + (begin + (asserts! (is-test-env) (err ERR_UNAUTHORIZED)) + (as-contract (try! (contract-call? coreContract set-city-wallet (var-get cityWallet)))) + (ok true) + ) +) diff --git a/contracts/legacy/miamicoin-core-v1-patch.clar b/contracts/legacy/miamicoin-core-v1-patch.clar new file mode 100644 index 00000000..0773b6a1 --- /dev/null +++ b/contracts/legacy/miamicoin-core-v1-patch.clar @@ -0,0 +1,67 @@ +;; MIAMICOIN CORE CONTRACT V1 PATCH +;; CityCoins Protocol Version 2.0.0 + +(impl-trait .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 .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? .miamicoin-token burn amount owner))) + (ok true) + ) +) diff --git a/contracts/legacy/miamicoin-core-v1.clar b/contracts/legacy/miamicoin-core-v1.clar new file mode 100644 index 00000000..d53503b7 --- /dev/null +++ b/contracts/legacy/miamicoin-core-v1.clar @@ -0,0 +1,936 @@ +;; MIAMICOIN CORE CONTRACT V1 +;; CityCoins Protocol Version 1.0.0 + +;; GENERAL CONFIGURATION + +(impl-trait .citycoin-core-trait.citycoin-core) +(define-constant CONTRACT_OWNER tx-sender) + +;; ERROR CODES + +(define-constant ERR_UNAUTHORIZED u1000) +(define-constant ERR_USER_ALREADY_REGISTERED u1001) +(define-constant ERR_USER_NOT_FOUND u1002) +(define-constant ERR_USER_ID_NOT_FOUND u1003) +(define-constant ERR_ACTIVATION_THRESHOLD_REACHED u1004) +(define-constant ERR_CONTRACT_NOT_ACTIVATED u1005) +(define-constant ERR_USER_ALREADY_MINED u1006) +(define-constant ERR_INSUFFICIENT_COMMITMENT u1007) +(define-constant ERR_INSUFFICIENT_BALANCE u1008) +(define-constant ERR_USER_DID_NOT_MINE_IN_BLOCK u1009) +(define-constant ERR_CLAIMED_BEFORE_MATURITY u1010) +(define-constant ERR_NO_MINERS_AT_BLOCK u1011) +(define-constant ERR_REWARD_ALREADY_CLAIMED u1012) +(define-constant ERR_MINER_DID_NOT_WIN u1013) +(define-constant ERR_NO_VRF_SEED_FOUND u1014) +(define-constant ERR_STACKING_NOT_AVAILABLE u1015) +(define-constant ERR_CANNOT_STACK u1016) +(define-constant ERR_REWARD_CYCLE_NOT_COMPLETED u1017) +(define-constant ERR_NOTHING_TO_REDEEM u1018) +(define-constant ERR_UNABLE_TO_FIND_CITY_WALLET u1019) +(define-constant ERR_CLAIM_IN_WRONG_CONTRACT u1020) + +;; CITY WALLET MANAGEMENT + +;; initial value for city wallet, set to this contract until initialized +(define-data-var cityWallet principal .miamicoin-core-v1) + +;; 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 ERR_UNAUTHORIZED)) + (ok (var-set cityWallet newCityWallet)) + ) +) + +;; REGISTRATION + +(define-data-var activationBlock uint u340282366920938463463374607431768211455) +(define-data-var activationDelay uint u150) +(define-data-var activationReached bool false) +(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) + (let + ( + (activated (var-get activationReached)) + ) + (asserts! activated (err 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 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? .miamicoin-auth is-initialized)) + ) + + (asserts! initialized (err ERR_UNAUTHORIZED)) + + (asserts! (is-none (map-get? UserIds tx-sender)) + (err ERR_USER_ALREADY_REGISTERED)) + + (asserts! (<= newId threshold) + (err ERR_ACTIVATION_THRESHOLD_REACHED)) + + (if (is-some memo) + (print memo) + none + ) + + (get-or-create-user-id tx-sender) + + (if (is-eq newId threshold) + (let + ( + (activationBlockVal (+ block-height (var-get activationDelay))) + ) + (try! (contract-call? .miamicoin-auth activate-core-contract (as-contract tx-sender) activationBlockVal)) + (try! (contract-call? .miamicoin-token activate-token (as-contract tx-sender) activationBlockVal)) + (try! (set-coinbase-thresholds)) + (var-set activationReached true) + (var-set activationBlock activationBlockVal) + (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! (get-activation-status) (err ERR_CONTRACT_NOT_ACTIVATED)) + (asserts! (> (len amounts) u0) (err 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 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))) + (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 ERR_USER_ALREADY_MINED)) + (asserts! (> amountUstx u0) (err 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! (get-activation-status) (err ERR_CONTRACT_NOT_ACTIVATED)) + (asserts! (not (has-mined-at-block stacksHeight userId)) (err ERR_USER_ALREADY_MINED)) + (asserts! (> amountUstx u0) (err ERR_INSUFFICIENT_COMMITMENT)) + (asserts! (>= (stx-get-balance tx-sender) amountUstx) (err 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 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 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 ERR_USER_ID_NOT_FOUND))) + (blockStats (unwrap! (get-mining-stats-at-block minerBlockHeight) (err ERR_NO_MINERS_AT_BLOCK))) + (minerStats (unwrap! (get-miner-at-block minerBlockHeight userId) (err ERR_USER_DID_NOT_MINE_IN_BLOCK))) + (isMature (asserts! (> stacksHeight maturityHeight) (err ERR_CLAIMED_BEFORE_MATURITY))) + (vrfSample (unwrap! (contract-call? .citycoin-vrf get-random-uint-at-block maturityHeight) (err ERR_NO_VRF_SEED_FOUND))) + (commitTotal (get-last-high-value-at-block minerBlockHeight)) + (winningValue (mod vrfSample commitTotal)) + ) + (asserts! (not (get rewardClaimed blockStats)) (err ERR_REWARD_ALREADY_CLAIMED)) + (asserts! (and (>= winningValue (get lowValue minerStats)) (<= winningValue (get highValue minerStats))) + (err 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 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? .citycoin-vrf get-random-uint-at-block 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 (or (<= currentCycle targetCycle) (is-eq u0 userStackedThisCycle)) + ;; 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 ERR_STACKING_NOT_AVAILABLE))) + (targetCycle (+ u1 currentCycle)) + (commitment { + stackerId: userId, + amount: amountTokens, + first: targetCycle, + last: (+ targetCycle lockPeriod) + }) + ) + (asserts! (get-activation-status) (err ERR_CONTRACT_NOT_ACTIVATED)) + (asserts! (and (> lockPeriod u0) (<= lockPeriod MAX_REWARD_CYCLES)) + (err ERR_CANNOT_STACK)) + (asserts! (> amountTokens u0) (err ERR_CANNOT_STACK)) + (try! (contract-call? .miamicoin-token transfer amountTokens tx-sender (as-contract tx-sender) none)) + (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)) + (stackerAtCycle (get-stacker-at-cycle-or-default targetCycle stackerId)) + (amountStacked (get amountStacked stackerAtCycle)) + (toReturn (get toReturn stackerAtCycle)) + ) + (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 ERR_STACKING_NOT_AVAILABLE))) + (userId (unwrap! (get-user-id user) (err 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 ERR_REWARD_CYCLE_NOT_COMPLETED)) + (asserts! (or (> toReturn u0) (> entitledUstx u0)) (err 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? .miamicoin-token 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 + +;; 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) + +(define-private (set-coinbase-thresholds) + (let + ( + (coinbaseAmounts (try! (contract-call? .miamicoin-token get-coinbase-thresholds))) + ) + (var-set coinbaseThreshold1 (get coinbaseThreshold1 coinbaseAmounts)) + (var-set coinbaseThreshold2 (get coinbaseThreshold2 coinbaseAmounts)) + (var-set coinbaseThreshold3 (get coinbaseThreshold3 coinbaseAmounts)) + (var-set coinbaseThreshold4 (get coinbaseThreshold4 coinbaseAmounts)) + (var-set coinbaseThreshold5 (get coinbaseThreshold5 coinbaseAmounts)) + (ok true) + ) +) + +;; return coinbase thresholds if contract activated +(define-read-only (get-coinbase-thresholds) + (let + ( + (activated (var-get activationReached)) + ) + (asserts! activated (err 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) + }) + ) +) + +;; 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 issuance schedule + ;; halvings occur every 210,000 blocks for 1,050,000 Stacks blocks + ;; then mining continues indefinitely with 3,125 tokens as the reward + (asserts! (> minerBlockHeight (var-get coinbaseThreshold1)) + (if (<= (- minerBlockHeight (var-get activationBlock)) u10000) + ;; bonus reward first 10,000 blocks + u250000 + ;; standard reward remaining 200,000 blocks until 1st halving + u100000 + ) + ) + ;; computations based on each halving threshold + (asserts! (> minerBlockHeight (var-get coinbaseThreshold2)) u50000) + (asserts! (> minerBlockHeight (var-get coinbaseThreshold3)) u25000) + (asserts! (> minerBlockHeight (var-get coinbaseThreshold4)) u12500) + (asserts! (> minerBlockHeight (var-get coinbaseThreshold5)) u6250) + ;; default value after 5th halving + u3125 + ) +) + +;; mint new tokens for claimant who won at given Stacks block height +(define-private (mint-coinbase (recipient principal) (stacksHeight uint)) + (as-contract (contract-call? .miamicoin-token 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 + ;; only allow shutdown request from AUTH + (asserts! (is-authorized-auth) (err 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 .miamicoin-auth) +) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; TESTING FUNCTIONS +;; DELETE BEFORE DEPLOYING TO MAINNET +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(define-constant DEPLOYED_AT block-height) + +(define-private (is-test-env) + (<= DEPLOYED_AT u5) +) + +(use-trait coreTrait .citycoin-core-trait.citycoin-core) + +(define-public (test-set-city-wallet (newCityWallet principal)) + (begin + (asserts! (is-test-env) (err ERR_UNAUTHORIZED)) + (ok (var-set cityWallet newCityWallet)) + ) +) + +(define-public (test-set-activation-threshold (newThreshold uint)) + (begin + (asserts! (is-test-env) (err ERR_UNAUTHORIZED)) + (ok (var-set activationThreshold newThreshold)) + ) +) + +(define-public (test-initialize-core (coreContract )) + (begin + (asserts! (is-test-env) (err ERR_UNAUTHORIZED)) + (var-set activationThreshold u1) + (try! (contract-call? .miamicoin-auth test-initialize-contracts coreContract)) + (ok true) + ) +) + +(define-public (test-burn (amount uint) (recipient principal)) + (begin + (asserts! (is-test-env) (err ERR_UNAUTHORIZED)) + (as-contract (try! (contract-call? .miamicoin-token burn amount recipient))) + (ok true) + ) +) diff --git a/contracts/legacy/miamicoin-token.clar b/contracts/legacy/miamicoin-token.clar new file mode 100644 index 00000000..96f78737 --- /dev/null +++ b/contracts/legacy/miamicoin-token.clar @@ -0,0 +1,200 @@ +;; MIAMICOIN TOKEN CONTRACT +;; CityCoins Protocol Version 1.0.0 + +;; CONTRACT OWNER + +(define-constant CONTRACT_OWNER tx-sender) + +;; TRAIT DEFINITIONS + +(impl-trait .citycoin-token-trait.citycoin-token) +(use-trait coreTrait .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 'ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.sip-010-trait-ft-standard.sip-010-trait) +;; MAINNET +;; (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? .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? .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? .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 .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) + ) +) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; TESTING FUNCTIONS +;; DELETE BEFORE DEPLOYING TO MAINNET +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(define-constant DEPLOYED_AT block-height) + +(define-private (is-test-env) + (<= DEPLOYED_AT u5) +) + +(define-public (test-mint (amount uint) (recipient principal)) + (begin + (asserts! (is-test-env) (err ERR_UNAUTHORIZED)) + (ft-mint? miamicoin amount recipient) + ) +) diff --git a/contracts/legacy/newyorkcitycoin-auth.clar b/contracts/legacy/newyorkcitycoin-auth.clar new file mode 100644 index 00000000..88bf302a --- /dev/null +++ b/contracts/legacy/newyorkcitycoin-auth.clar @@ -0,0 +1,619 @@ +;; NEWYORKCITYCOIN AUTH CONTRACT +;; CityCoins Protocol Version 1.0.1 + +(define-constant CONTRACT_OWNER tx-sender) + +;; TRAIT DEFINITIONS + +(use-trait coreTrait .citycoin-core-trait.citycoin-core) +(use-trait tokenTrait .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 'STEB8ZW46YZJ40E3P7A287RBJFWPHYNQ2AB5ECT8) +;; MAINNET +;; (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 'ST1J4G6RR643BCG8G8SR6M2D9Z9KXT2NJDRK3FBTK true) +(map-insert Approvers 'ST20ATRN26N9P05V2F1RHFRV24X8C8M3W54E427B2 true) +(map-insert Approvers 'ST21HMSJATHZ888PD0S0SSTWP4J61TCRJYEVQ0STB true) +(map-insert Approvers 'ST2QXSK64YQX3CQPC530K79XWQ98XFAM9W3XKEH3N true) +(map-insert Approvers 'ST3DG3R65C9TTEEW5BC5XTSY0M1JM7NBE7GVWKTVJ true) + +;; MAINNET +;; (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) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; TESTING FUNCTIONS +;; DELETE BEFORE DEPLOYING TO MAINNET +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(define-constant DEPLOYED_AT block-height) + +(define-private (is-test-env) + (<= DEPLOYED_AT u5) +) + +(define-public (test-initialize-contracts (coreContract )) + (let + ( + (coreContractAddress (contract-of coreContract)) + ) + (asserts! (is-test-env) (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-public (test-set-active-core-contract) + (begin + (asserts! (is-test-env) (err ERR_UNAUTHORIZED)) + (ok (var-set activeCoreContract .newyorkcitycoin-core-v1)) + ) +) + +(define-public (test-set-city-wallet-patch (coreContract )) + (begin + (asserts! (is-test-env) (err ERR_UNAUTHORIZED)) + (as-contract (try! (contract-call? coreContract set-city-wallet (var-get cityWallet)))) + (ok true) + ) +) diff --git a/contracts/legacy/newyorkcitycoin-core-v1-patch.clar b/contracts/legacy/newyorkcitycoin-core-v1-patch.clar new file mode 100644 index 00000000..a6c1554a --- /dev/null +++ b/contracts/legacy/newyorkcitycoin-core-v1-patch.clar @@ -0,0 +1,54 @@ +;; NEWYORKCITYCOIN CORE CONTRACT V1 PATCH +;; CityCoins Protocol Version 2.0.0 + +(impl-trait .citycoin-core-trait.citycoin-core) + +;; uses same and skips errors already defined in newyorkcitycoin-core-v1 +(define-constant ERR_UNAUTHORIZED (err u1001)) +;; generic error used to disable all functions below +(define-constant ERR_CONTRACT_DISABLED (err u1021)) + +(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 .newyorkcitycoin-auth) +) diff --git a/contracts/legacy/newyorkcitycoin-core-v1.clar b/contracts/legacy/newyorkcitycoin-core-v1.clar new file mode 100644 index 00000000..d78bb6ed --- /dev/null +++ b/contracts/legacy/newyorkcitycoin-core-v1.clar @@ -0,0 +1,936 @@ +;; NEWYORKCITYCOIN CORE CONTRACT +;; CityCoins Protocol Version 1.0.1 + +;; GENERAL CONFIGURATION + +(impl-trait .citycoin-core-trait.citycoin-core) +(define-constant CONTRACT_OWNER tx-sender) + +;; ERROR CODES + +(define-constant ERR_UNAUTHORIZED u1000) +(define-constant ERR_USER_ALREADY_REGISTERED u1001) +(define-constant ERR_USER_NOT_FOUND u1002) +(define-constant ERR_USER_ID_NOT_FOUND u1003) +(define-constant ERR_ACTIVATION_THRESHOLD_REACHED u1004) +(define-constant ERR_CONTRACT_NOT_ACTIVATED u1005) +(define-constant ERR_USER_ALREADY_MINED u1006) +(define-constant ERR_INSUFFICIENT_COMMITMENT u1007) +(define-constant ERR_INSUFFICIENT_BALANCE u1008) +(define-constant ERR_USER_DID_NOT_MINE_IN_BLOCK u1009) +(define-constant ERR_CLAIMED_BEFORE_MATURITY u1010) +(define-constant ERR_NO_MINERS_AT_BLOCK u1011) +(define-constant ERR_REWARD_ALREADY_CLAIMED u1012) +(define-constant ERR_MINER_DID_NOT_WIN u1013) +(define-constant ERR_NO_VRF_SEED_FOUND u1014) +(define-constant ERR_STACKING_NOT_AVAILABLE u1015) +(define-constant ERR_CANNOT_STACK u1016) +(define-constant ERR_REWARD_CYCLE_NOT_COMPLETED u1017) +(define-constant ERR_NOTHING_TO_REDEEM u1018) +(define-constant ERR_UNABLE_TO_FIND_CITY_WALLET u1019) +(define-constant ERR_CLAIM_IN_WRONG_CONTRACT u1020) + +;; CITY WALLET MANAGEMENT + +;; initial value for city wallet, set to this contract until initialized +(define-data-var cityWallet principal .newyorkcitycoin-core-v1) + +;; 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 ERR_UNAUTHORIZED)) + (ok (var-set cityWallet newCityWallet)) + ) +) + +;; REGISTRATION + +(define-data-var activationBlock uint u340282366920938463463374607431768211455) +(define-data-var activationDelay uint u150) +(define-data-var activationReached bool false) +(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) + (let + ( + (activated (var-get activationReached)) + ) + (asserts! activated (err 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 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? .newyorkcitycoin-auth is-initialized)) + ) + + (asserts! initialized (err ERR_UNAUTHORIZED)) + + (asserts! (is-none (map-get? UserIds tx-sender)) + (err ERR_USER_ALREADY_REGISTERED)) + + (asserts! (<= newId threshold) + (err ERR_ACTIVATION_THRESHOLD_REACHED)) + + (if (is-some memo) + (print memo) + none + ) + + (get-or-create-user-id tx-sender) + + (if (is-eq newId threshold) + (let + ( + (activationBlockVal (+ block-height (var-get activationDelay))) + ) + (try! (contract-call? .newyorkcitycoin-auth activate-core-contract (as-contract tx-sender) activationBlockVal)) + (try! (contract-call? .newyorkcitycoin-token activate-token (as-contract tx-sender) activationBlockVal)) + (try! (set-coinbase-thresholds)) + (var-set activationReached true) + (var-set activationBlock activationBlockVal) + (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! (get-activation-status) (err ERR_CONTRACT_NOT_ACTIVATED)) + (asserts! (> (len amounts) u0) (err 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 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 ERR_USER_ALREADY_MINED)) + (asserts! (> amountUstx u0) (err 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! (get-activation-status) (err ERR_CONTRACT_NOT_ACTIVATED)) + (asserts! (not (has-mined-at-block stacksHeight userId)) (err ERR_USER_ALREADY_MINED)) + (asserts! (> amountUstx u0) (err ERR_INSUFFICIENT_COMMITMENT)) + (asserts! (>= (stx-get-balance tx-sender) amountUstx) (err 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 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 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 ERR_USER_ID_NOT_FOUND))) + (blockStats (unwrap! (get-mining-stats-at-block minerBlockHeight) (err ERR_NO_MINERS_AT_BLOCK))) + (minerStats (unwrap! (get-miner-at-block minerBlockHeight userId) (err ERR_USER_DID_NOT_MINE_IN_BLOCK))) + (isMature (asserts! (> stacksHeight maturityHeight) (err ERR_CLAIMED_BEFORE_MATURITY))) + (vrfSample (unwrap! (contract-call? .citycoin-vrf get-random-uint-at-block maturityHeight) (err ERR_NO_VRF_SEED_FOUND))) + (commitTotal (get-last-high-value-at-block minerBlockHeight)) + (winningValue (mod vrfSample commitTotal)) + ) + (asserts! (not (get rewardClaimed blockStats)) (err ERR_REWARD_ALREADY_CLAIMED)) + (asserts! (and (>= winningValue (get lowValue minerStats)) (<= winningValue (get highValue minerStats))) + (err 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 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? .citycoin-vrf get-random-uint-at-block 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 (or (<= currentCycle targetCycle) (is-eq u0 userStackedThisCycle)) + ;; 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 ERR_STACKING_NOT_AVAILABLE))) + (targetCycle (+ u1 currentCycle)) + (commitment { + stackerId: userId, + amount: amountTokens, + first: targetCycle, + last: (+ targetCycle lockPeriod) + }) + ) + (asserts! (get-activation-status) (err ERR_CONTRACT_NOT_ACTIVATED)) + (asserts! (and (> lockPeriod u0) (<= lockPeriod MAX_REWARD_CYCLES)) + (err ERR_CANNOT_STACK)) + (asserts! (> amountTokens u0) (err ERR_CANNOT_STACK)) + (try! (contract-call? .newyorkcitycoin-token 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)) + (stackerAtCycle (get-stacker-at-cycle-or-default targetCycle stackerId)) + (amountStacked (get amountStacked stackerAtCycle)) + (toReturn (get toReturn stackerAtCycle)) + ) + (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 ERR_STACKING_NOT_AVAILABLE))) + (userId (unwrap! (get-user-id user) (err 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 ERR_REWARD_CYCLE_NOT_COMPLETED)) + (asserts! (or (> toReturn u0) (> entitledUstx u0)) (err 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? .newyorkcitycoin-token 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 + +;; 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) + +(define-private (set-coinbase-thresholds) + (let + ( + (coinbaseAmounts (try! (contract-call? .newyorkcitycoin-token get-coinbase-thresholds))) + ) + (var-set coinbaseThreshold1 (get coinbaseThreshold1 coinbaseAmounts)) + (var-set coinbaseThreshold2 (get coinbaseThreshold2 coinbaseAmounts)) + (var-set coinbaseThreshold3 (get coinbaseThreshold3 coinbaseAmounts)) + (var-set coinbaseThreshold4 (get coinbaseThreshold4 coinbaseAmounts)) + (var-set coinbaseThreshold5 (get coinbaseThreshold5 coinbaseAmounts)) + (ok true) + ) +) + +;; return coinbase thresholds if contract activated +(define-read-only (get-coinbase-thresholds) + (let + ( + (activated (var-get activationReached)) + ) + (asserts! activated (err 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) + }) + ) +) + +;; 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 issuance schedule + ;; halvings occur every 210,000 blocks for 1,050,000 Stacks blocks + ;; then mining continues indefinitely with 3,125 tokens as the reward + (asserts! (> minerBlockHeight (var-get coinbaseThreshold1)) + (if (<= (- minerBlockHeight (var-get activationBlock)) u10000) + ;; bonus reward first 10,000 blocks + u250000 + ;; standard reward remaining 200,000 blocks until 1st halving + u100000 + ) + ) + ;; computations based on each halving threshold + (asserts! (> minerBlockHeight (var-get coinbaseThreshold2)) u50000) + (asserts! (> minerBlockHeight (var-get coinbaseThreshold3)) u25000) + (asserts! (> minerBlockHeight (var-get coinbaseThreshold4)) u12500) + (asserts! (> minerBlockHeight (var-get coinbaseThreshold5)) u6250) + ;; default value after 5th halving + u3125 + ) +) + +;; mint new tokens for claimant who won at given Stacks block height +(define-private (mint-coinbase (recipient principal) (stacksHeight uint)) + (as-contract (contract-call? .newyorkcitycoin-token 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 + ;; only allow shutdown request from AUTH + (asserts! (is-authorized-auth) (err 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 .newyorkcitycoin-auth) +) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; TESTING FUNCTIONS +;; DELETE BEFORE DEPLOYING TO MAINNET +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(define-constant DEPLOYED_AT block-height) + +(define-private (is-test-env) + (<= DEPLOYED_AT u5) +) + +(use-trait coreTrait .citycoin-core-trait.citycoin-core) + +(define-public (test-set-city-wallet (newCityWallet principal)) + (begin + (asserts! (is-test-env) (err ERR_UNAUTHORIZED)) + (ok (var-set cityWallet newCityWallet)) + ) +) + +(define-public (test-set-activation-threshold (newThreshold uint)) + (begin + (asserts! (is-test-env) (err ERR_UNAUTHORIZED)) + (ok (var-set activationThreshold newThreshold)) + ) +) + +(define-public (test-initialize-core (coreContract )) + (begin + (asserts! (is-test-env) (err ERR_UNAUTHORIZED)) + (var-set activationThreshold u1) + (try! (contract-call? .newyorkcitycoin-auth test-initialize-contracts coreContract)) + (ok true) + ) +) diff --git a/contracts/legacy/newyorkcitycoin-token.clar b/contracts/legacy/newyorkcitycoin-token.clar new file mode 100644 index 00000000..0dd05028 --- /dev/null +++ b/contracts/legacy/newyorkcitycoin-token.clar @@ -0,0 +1,197 @@ +;; NEWYORKCITYCOIN TOKEN CONTRACT +;; CityCoins Protocol Version 1.0.1 + +;; CONTRACT OWNER + +(define-constant CONTRACT_OWNER tx-sender) + +;; TRAIT DEFINITIONS + +(impl-trait .citycoin-token-trait.citycoin-token) +(use-trait coreTrait .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 'ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.sip-010-trait-ft-standard.sip-010-trait) +;; MAINNET +;; (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? .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? .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 .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) + ) +) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; TESTING FUNCTIONS +;; DELETE BEFORE DEPLOYING TO MAINNET +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(define-constant DEPLOYED_AT block-height) + +(define-private (is-test-env) + (<= DEPLOYED_AT u5) +) + +(define-public (test-mint (amount uint) (recipient principal)) + (begin + (asserts! (is-test-env) (err ERR_UNAUTHORIZED)) + (ft-mint? newyorkcitycoin amount recipient) + ) +) diff --git a/contracts/proposals/ccip020-graceful-protocol-shutdown.clar b/contracts/proposals/ccip020-graceful-protocol-shutdown.clar new file mode 100644 index 00000000..0c3cdc0e --- /dev/null +++ b/contracts/proposals/ccip020-graceful-protocol-shutdown.clar @@ -0,0 +1,315 @@ +;; TRAITS + +(impl-trait .proposal-trait.proposal-trait) +(impl-trait .ccip015-trait.ccip015-trait) + +;; ERRORS + +(define-constant ERR_PANIC (err u2000)) +(define-constant ERR_VOTED_ALREADY (err u2001)) +(define-constant ERR_NOTHING_STACKED (err u2002)) +(define-constant ERR_USER_NOT_FOUND (err u2003)) +(define-constant ERR_PROPOSAL_NOT_ACTIVE (err u2004)) +(define-constant ERR_PROPOSAL_STILL_ACTIVE (err u2005)) +(define-constant ERR_VOTE_FAILED (err u2006)) + +;; CONSTANTS + +(define-constant CCIP_020 { + name: "Graceful Protocol Shutdown", + link: "https://github.com/citycoins/governance/blob/feat/add-ccip-020/ccips/ccip-020/ccip-020-graceful-protocol-shutdown.md", + hash: "f97f90b74d2e75b2481bbb5f768bc238ded68be0", +}) + +(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") + +;; MIA votes scaled to make 1 MIA = 1 NYC +;; full calculation available in CCIP-020 +(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% + +;; 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) + ;; disable mining and stacking contracts + (try! (contract-call? .ccd006-citycoin-mining-v2 set-mining-enabled false)) + (try! (contract-call? .ccd007-citycoin-stacking set-stacking-enabled false)) + (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) +) diff --git a/models/proposals/ccip020-graceful-protocol-shutdown.model.ts b/models/proposals/ccip020-graceful-protocol-shutdown.model.ts new file mode 100644 index 00000000..778e4a5e --- /dev/null +++ b/models/proposals/ccip020-graceful-protocol-shutdown.model.ts @@ -0,0 +1,87 @@ +import { Chain, Account, Tx, types, ReadOnlyFn } from "../../utils/deps.ts"; + +enum ErrCode { + ERR_PANIC = 2000, + ERR_VOTED_ALREADY, + ERR_NOTHING_STACKED, + ERR_USER_NOT_FOUND, + ERR_PROPOSAL_NOT_ACTIVE, + ERR_PROPOSAL_STILL_ACTIVE, + ERR_VOTE_FAILED, +} + +export class CCIP020GracefulProtocolShutdown { + name = "ccip020-graceful-protocol-shutdown"; + 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"); + } + + getVoteTotalMia() { + return this.callReadOnlyFn("get-vote-total-mia"); + } + + getVoteTotalMiaOrDefault() { + return this.callReadOnlyFn("get-vote-total-mia-or-default"); + } + + 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)]); + } + + getMiaVote(userId: number, scaled: boolean) { + return this.callReadOnlyFn("get-mia-vote", [types.uint(userId), types.bool(scaled)]); + } + + 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-ccd007-citycoin-stacking-009.clar b/tests/contracts/proposals/test-ccd007-citycoin-stacking-009.clar index 0bbb22ea..8e2fba42 100644 --- a/tests/contracts/proposals/test-ccd007-citycoin-stacking-009.clar +++ b/tests/contracts/proposals/test-ccd007-citycoin-stacking-009.clar @@ -9,6 +9,11 @@ (begin (try! (contract-call? .test-ccext-governance-token-mia mint u1000 'ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5)) (try! (contract-call? .test-ccext-governance-token-mia mint u1000 'ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG)) + ;; added 2024-04-12 to mint 3rd user + mint NYC as well + (try! (contract-call? .test-ccext-governance-token-mia mint u1000 'ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC)) + (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)) (ok true) ) ) diff --git a/tests/contracts/proposals/test-ccd007-citycoin-stacking-010.clar b/tests/contracts/proposals/test-ccd007-citycoin-stacking-010.clar index 301029d3..03de75bb 100644 --- a/tests/contracts/proposals/test-ccd007-citycoin-stacking-010.clar +++ b/tests/contracts/proposals/test-ccd007-citycoin-stacking-010.clar @@ -8,6 +8,8 @@ (define-public (execute (sender principal)) (begin (try! (contract-call? .ccd002-treasury-mia-stacking set-allowed .test-ccext-governance-token-mia true)) + ;; added 2024-04-12 to add NYC as well + (try! (contract-call? .ccd002-treasury-nyc-stacking set-allowed .test-ccext-governance-token-nyc true)) (ok true) ) ) diff --git a/tests/contracts/proposals/test-ccip014-pox-3-001.clar b/tests/contracts/proposals/test-ccip014-pox-3-001.clar index 0bc303ad..df71dbcc 100644 --- a/tests/contracts/proposals/test-ccip014-pox-3-001.clar +++ b/tests/contracts/proposals/test-ccip014-pox-3-001.clar @@ -26,8 +26,10 @@ ;; 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-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)) ;; 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)) diff --git a/tests/contracts/proposals/test-ccip014-pox-3-002.clar b/tests/contracts/proposals/test-ccip014-pox-3-002.clar index 4bb6f171..bae289de 100644 --- a/tests/contracts/proposals/test-ccip014-pox-3-002.clar +++ b/tests/contracts/proposals/test-ccip014-pox-3-002.clar @@ -14,6 +14,10 @@ (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/contracts/proposals/test-ccip020-shutdown-001.clar b/tests/contracts/proposals/test-ccip020-shutdown-001.clar new file mode 100644 index 00000000..5b089d66 --- /dev/null +++ b/tests/contracts/proposals/test-ccip020-shutdown-001.clar @@ -0,0 +1,26 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer +;; Description: +;; Pays out cycle 5 as pool operator for MIA and NYC + +(impl-trait .proposal-trait.proposal-trait) + +(define-constant SELF (as-contract tx-sender)) +(define-constant PAYOUT u1000000000) ;; 1,000 STX + +(define-public (execute (sender principal)) + (begin + ;; set pool operator to self + (try! (contract-call? .ccd011-stacking-payouts set-pool-operator SELF)) + ;; pay out cycle 5 + (as-contract (try! (contract-call? .ccd011-stacking-payouts send-stacking-reward-mia u5 PAYOUT))) + (as-contract (try! (contract-call? .ccd011-stacking-payouts send-stacking-reward-nyc u5 PAYOUT))) + ;; restore pool operator + (try! (contract-call? .ccd011-stacking-payouts set-pool-operator 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6)) + (ok true) + ) +) + +;; fund contract for payouts +(stx-transfer? (* PAYOUT u2) tx-sender SELF) diff --git a/tests/extensions/ccd006-citycoin-mining.test.ts b/tests/extensions/ccd006-citycoin-mining.test.ts index 930472fd..ed0b1fba 100644 --- a/tests/extensions/ccd006-citycoin-mining.test.ts +++ b/tests/extensions/ccd006-citycoin-mining.test.ts @@ -263,8 +263,9 @@ Clarinet.test({ const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining"); const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); // balance is 99999999999992 after ccip-014 deployment - const expectedBalance = 99999999999992; - const entries = [49999999999996, 49999999999996]; + // new balance is 99997999999992 after ccip-020 test deployment + const expectedBalance = 99997999999992; + const entries = [49998999999996, 49998999999996]; // act diff --git a/tests/proposals/ccip020-graceful-protocol-shutdown.test.ts b/tests/proposals/ccip020-graceful-protocol-shutdown.test.ts new file mode 100644 index 00000000..b211d775 --- /dev/null +++ b/tests/proposals/ccip020-graceful-protocol-shutdown.test.ts @@ -0,0 +1,940 @@ +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 { CCIP020GracefulProtocolShutdown } from "../../models/proposals/ccip020-graceful-protocol-shutdown.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, ccip020: CCIP020GracefulProtocolShutdown) { + console.log("contract vote totals mia:"); + console.log(JSON.stringify(ccip020.getVoteTotalMia(), null, 2)); + console.log("contract vote totals nyc:"); + console.log(JSON.stringify(ccip020.getVoteTotalNyc(), null, 2)); + console.log("contract vote totals:"); + console.log(JSON.stringify(ccip020.getVoteTotals(), null, 2)); + + console.log("user 1:"); + console.log(ccip020.getVoterInfo(1)); + console.log("user 1 MIA:"); + console.log(ccd007.getStacker(mia.cityId, 2, 1)); + console.log(ccip020.getMiaVote(1, false)); + console.log(ccip020.getMiaVote(1, true)); + console.log("user 1 NYC:"); + console.log(ccd007.getStacker(nyc.cityId, 2, 1)); + console.log(ccip020.getNycVote(1, false)); + console.log(ccip020.getNycVote(1, true)); + + console.log("user 2:"); + console.log(ccip020.getVoterInfo(2)); + console.log("user 2 MIA:"); + console.log(ccd007.getStacker(mia.cityId, 2, 2)); + console.log(ccip020.getMiaVote(2, false)); + console.log(ccip020.getMiaVote(2, true)); + console.log("user 2 NYC:"); + console.log(ccd007.getStacker(nyc.cityId, 2, 2)); + console.log(ccip020.getNycVote(2, false)); + console.log(ccip020.getNycVote(2, true)); + + console.log("user 3:"); + console.log(ccip020.getVoterInfo(3)); + console.log("user 3 MIA:"); + console.log(ccd007.getStacker(mia.cityId, 2, 3)); + console.log(ccip020.getMiaVote(3, false)); + console.log(ccip020.getMiaVote(3, true)); + console.log("user 3 NYC:"); + console.log(ccd007.getStacker(nyc.cityId, 2, 3)); + console.log(ccip020.getNycVote(3, false)); + console.log(ccip020.getNycVote(3, true)); +} + +Clarinet.test({ + name: "ccip-020: execute() fails with ERR_VOTE_FAILED if there are no votes", + fn(chain: Chain, accounts: Map) { + // arrange + + // register MIA and NYC + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + // set activation details for MIA and NYC + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + // set activation status for MIA and NYC + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + + // act + + // execute ccip-020 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_020); + + // assert + block.receipts[2].result.expectErr().expectUint(CCIP020GracefulProtocolShutdown.ErrCode.ERR_VOTE_FAILED); + }, +}); + +Clarinet.test({ + name: "ccip-020: 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 ccip020GracefulProtocolShutdown = new CCIP020GracefulProtocolShutdown(chain, sender); + + const amountStacked = 500; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + // register MIA and NYC + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + // set activation details for MIA and NYC + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + // set activation status for MIA and NYC + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + // add stacking treasury in city data + passProposal(chain, accounts, PROPOSALS.TEST_CCD007_CITY_STACKING_007); + // mints mia to user1 and user2 + passProposal(chain, accounts, PROPOSALS.TEST_CCD007_CITY_STACKING_009); + // adds the token contract to the treasury allow list + passProposal(chain, accounts, PROPOSALS.TEST_CCD007_CITY_STACKING_010); + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, mia.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, mia.cityName, amountStacked, lockPeriod)]); + // 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([ccip020GracefulProtocolShutdown.voteOnProposal(user1, false), ccip020GracefulProtocolShutdown.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, ccip020GracefulProtocolShutdown); + */ + + // execute ccip-020 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_020); + + // assert + block.receipts[2].result.expectErr().expectUint(CCIP020GracefulProtocolShutdown.ErrCode.ERR_VOTE_FAILED); + }, +}); + +Clarinet.test({ + name: "ccip-020: execute() fails with ERR_VOTE_FAILED if MIA votes are more than 50% of the total 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 ccip020GracefulProtocolShutdown = new CCIP020GracefulProtocolShutdown(chain, sender); + + const amountStacked = 500; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + // register MIA and NYC + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + // set activation details for MIA and NYC + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + // set activation status for MIA and NYC + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + // add stacking treasury in city data + passProposal(chain, accounts, PROPOSALS.TEST_CCD007_CITY_STACKING_007); + passProposal(chain, accounts, PROPOSALS.TEST_CCD007_CITY_STACKING_008); + // mints mia and nyc to user1 and user2 + passProposal(chain, accounts, PROPOSALS.TEST_CCD007_CITY_STACKING_009); + // adds the token contracts to the treasury allow lists + passProposal(chain, accounts, PROPOSALS.TEST_CCD007_CITY_STACKING_010); + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, mia.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, mia.cityName, amountStacked, lockPeriod)]); + stackingBlock.receipts[0].result.expectOk().expectBool(true); + stackingBlock.receipts[1].result.expectOk().expectBool(true); + + // progress the chain to cycle 5 + // votes are counted in cycles 2-3 + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); + + // act + + // execute two yes votes with MIA only + const votingBlock = chain.mineBlock([ccip020GracefulProtocolShutdown.voteOnProposal(user1, true), ccip020GracefulProtocolShutdown.voteOnProposal(user2, true)]); + 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, ccip020GracefulProtocolShutdown); + */ + + // execute ccip-020 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_020); + + // assert + block.receipts[2].result.expectErr().expectUint(CCIP020GracefulProtocolShutdown.ErrCode.ERR_VOTE_FAILED); + }, +}); + +Clarinet.test({ + name: "ccip-020: execute() fails with ERR_VOTE_FAILED if NYC votes are more than 50% of the total 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 ccip020GracefulProtocolShutdown = new CCIP020GracefulProtocolShutdown(chain, sender); + + const amountStacked = 500; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + // register MIA and NYC + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + // set activation details for MIA and NYC + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + // set activation status for MIA and NYC + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + // add stacking treasury in city data + passProposal(chain, accounts, PROPOSALS.TEST_CCD007_CITY_STACKING_007); + passProposal(chain, accounts, PROPOSALS.TEST_CCD007_CITY_STACKING_008); + // mints mia and nyc to user1 and user2 + passProposal(chain, accounts, PROPOSALS.TEST_CCD007_CITY_STACKING_009); + // adds the token contracts to the treasury allow lists + passProposal(chain, accounts, PROPOSALS.TEST_CCD007_CITY_STACKING_010); + + // 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)]); + stackingBlock.receipts[0].result.expectOk().expectBool(true); + stackingBlock.receipts[1].result.expectOk().expectBool(true); + + // progress the chain to cycle 5 + // votes are counted in cycles 2-3 + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); + + // act + + // execute two yes votes with MIA only + const votingBlock = chain.mineBlock([ccip020GracefulProtocolShutdown.voteOnProposal(user1, true), ccip020GracefulProtocolShutdown.voteOnProposal(user2, true)]); + 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, ccip020GracefulProtocolShutdown); + */ + + // execute ccip-020 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_020); + + // assert + block.receipts[2].result.expectErr().expectUint(CCIP020GracefulProtocolShutdown.ErrCode.ERR_VOTE_FAILED); + }, +}); + +Clarinet.test({ + name: "ccip-020: execute() fails with ERR_VOTE_FAILED 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 ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip020GracefulProtocolShutdown = new CCIP020GracefulProtocolShutdown(chain, sender); + + const amountStacked = 500; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + // register MIA and NYC + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + // set activation details for MIA and NYC + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + // set activation status for MIA and NYC + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + // add stacking treasury in city data + passProposal(chain, accounts, PROPOSALS.TEST_CCD007_CITY_STACKING_007); + passProposal(chain, accounts, PROPOSALS.TEST_CCD007_CITY_STACKING_008); + // mints mia and nyc to user1 and user2 + passProposal(chain, accounts, PROPOSALS.TEST_CCD007_CITY_STACKING_009); + // adds the token contracts to the treasury allow lists + passProposal(chain, accounts, PROPOSALS.TEST_CCD007_CITY_STACKING_010); + + // 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); + + // act + + // execute two yes votes with MIA only + const votingBlock = chain.mineBlock([ccip020GracefulProtocolShutdown.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, ccip020GracefulProtocolShutdown); + */ + + // execute ccip-020 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_020); + + // assert + block.receipts[2].result.expectErr().expectUint(CCIP020GracefulProtocolShutdown.ErrCode.ERR_VOTE_FAILED); + }, +}); + +Clarinet.test({ + name: "ccip-020: 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 ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip020GracefulProtocolShutdown = new CCIP020GracefulProtocolShutdown(chain, sender); + + const amountStacked = 500; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + // register MIA and NYC + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + // set activation details for MIA and NYC + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + // set activation status for MIA and NYC + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + // add stacking treasury in city data + passProposal(chain, accounts, PROPOSALS.TEST_CCD007_CITY_STACKING_007); + passProposal(chain, accounts, PROPOSALS.TEST_CCD007_CITY_STACKING_008); + // mints mia and nyc to user1 and user2 + passProposal(chain, accounts, PROPOSALS.TEST_CCD007_CITY_STACKING_009); + // adds the token contracts to the treasury allow lists + passProposal(chain, accounts, PROPOSALS.TEST_CCD007_CITY_STACKING_010); + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, mia.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, mia.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, mia.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); + + // act + + // execute two yes votes, one no vote + const votingBlock = chain.mineBlock([ccip020GracefulProtocolShutdown.voteOnProposal(user1, true), ccip020GracefulProtocolShutdown.voteOnProposal(user2, true), ccip020GracefulProtocolShutdown.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, ccip020GracefulProtocolShutdown); + */ + + // execute ccip-020 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_020); + + // assert + block.receipts[2].result.expectOk().expectUint(3); + }, +}); + +Clarinet.test({ + name: "ccip-020: 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 ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip020GracefulProtocolShutdown = new CCIP020GracefulProtocolShutdown(chain, sender); + + const amountStacked = 500; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + // register MIA and NYC + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + // set activation details for MIA and NYC + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + // set activation status for MIA and NYC + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + // add stacking treasury in city data + passProposal(chain, accounts, PROPOSALS.TEST_CCD007_CITY_STACKING_007); + passProposal(chain, accounts, PROPOSALS.TEST_CCD007_CITY_STACKING_008); + // mints mia and nyc to user1 and user2 + passProposal(chain, accounts, PROPOSALS.TEST_CCD007_CITY_STACKING_009); + // adds the token contracts to the treasury allow lists + passProposal(chain, accounts, PROPOSALS.TEST_CCD007_CITY_STACKING_010); + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, mia.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, mia.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, mia.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); + + // act + + // execute two yes votes, one no vote + const votingBlock = chain.mineBlock([ccip020GracefulProtocolShutdown.voteOnProposal(user1, true), ccip020GracefulProtocolShutdown.voteOnProposal(user2, true), ccip020GracefulProtocolShutdown.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, ccip020GracefulProtocolShutdown); + */ + + const votingBlockReversed = chain.mineBlock([ccip020GracefulProtocolShutdown.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, ccip020GracefulProtocolShutdown); + */ + + // execute ccip-020 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_020); + + // assert + block.receipts[2].result.expectOk().expectUint(3); + }, +}); + +Clarinet.test({ + name: "ccip-020: vote-on-proposal() fails with ERR_USER_NOT_FOUND if user is not registered in ccd003-user-registry", + fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const user2 = accounts.get("wallet_2")!; + const user3 = accounts.get("wallet_3")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip020GracefulProtocolShutdown = new CCIP020GracefulProtocolShutdown(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_CCIP014_POX3_001); + constructBlock.receipts[0].result.expectOk().expectBool(true); + + // mine to put funds in the mining treasury + const miningBlock = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, miningEntries), ccd006CityMining.mine(sender, 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, mia.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, mia.cityName, amountStacked / 2, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked / 2, lockPeriod)]); + 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([ccip020GracefulProtocolShutdown.voteOnProposal(user1, true), ccip020GracefulProtocolShutdown.voteOnProposal(user2, true), ccip020GracefulProtocolShutdown.voteOnProposal(user3, true)]); + + // assert + votingBlock.receipts[0].result.expectOk().expectBool(true); + votingBlock.receipts[1].result.expectOk().expectBool(true); + votingBlock.receipts[2].result.expectErr().expectUint(CCIP020GracefulProtocolShutdown.ErrCode.ERR_USER_NOT_FOUND); + }, +}); + +Clarinet.test({ + name: "ccip-020: 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 ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip020GracefulProtocolShutdown = new CCIP020GracefulProtocolShutdown(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_CCIP014_POX3_001); + constructBlock.receipts[0].result.expectOk().expectBool(true); + + // mine to put funds in the mining treasury + const miningBlock = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, miningEntries), ccd006CityMining.mine(sender, 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, mia.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, mia.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, mia.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([ccip020GracefulProtocolShutdown.voteOnProposal(user1, true), ccip020GracefulProtocolShutdown.voteOnProposal(user2, true), ccip020GracefulProtocolShutdown.voteOnProposal(user3, false)]); + for (let i = 0; i < votingBlock.receipts.length; i++) { + votingBlock.receipts[i].result.expectOk().expectBool(true); + } + + // execute ccip-020, ending the vote + passProposal(chain, accounts, PROPOSALS.CCIP_020); + + // act + // user 1 tries to reverse their vote + const votingBlockAfter = chain.mineBlock([ccip020GracefulProtocolShutdown.voteOnProposal(user1, false)]); + + // assert + votingBlockAfter.receipts[0].result.expectErr().expectUint(CCIP020GracefulProtocolShutdown.ErrCode.ERR_PROPOSAL_NOT_ACTIVE); + }, +}); + +Clarinet.test({ + name: "ccip-020: vote-on-proposal() fails with ERR_VOTED_ALREADY if user already voted with the same value", + fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const user2 = accounts.get("wallet_2")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip020GracefulProtocolShutdown = new CCIP020GracefulProtocolShutdown(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_CCIP014_POX3_001); + constructBlock.receipts[0].result.expectOk().expectBool(true); + + // mine to put funds in the mining treasury + const miningBlock = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, miningEntries), ccd006CityMining.mine(sender, 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, mia.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, mia.cityName, amountStacked / 2, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked / 2, lockPeriod)]); + 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([ccip020GracefulProtocolShutdown.voteOnProposal(user1, true)]); + votingBlock.receipts[0].result.expectOk().expectBool(true); + + // act + const votingBlockDupe = chain.mineBlock([ccip020GracefulProtocolShutdown.voteOnProposal(user1, true)]); + + // assert + votingBlockDupe.receipts[0].result.expectErr().expectUint(CCIP020GracefulProtocolShutdown.ErrCode.ERR_VOTED_ALREADY); + }, +}); + +Clarinet.test({ + name: "ccip-020: 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 ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip020GracefulProtocolShutdown = new CCIP020GracefulProtocolShutdown(chain, sender); + + const amountStacked = 500; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + // register MIA and NYC + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + // set activation details for MIA and NYC + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + // set activation status for MIA and NYC + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + // add stacking treasury in city data + passProposal(chain, accounts, PROPOSALS.TEST_CCD007_CITY_STACKING_007); + passProposal(chain, accounts, PROPOSALS.TEST_CCD007_CITY_STACKING_008); + // mints mia and nyc to user1 and user2 + passProposal(chain, accounts, PROPOSALS.TEST_CCD007_CITY_STACKING_009); + // adds the token contracts to the treasury allow lists + passProposal(chain, accounts, PROPOSALS.TEST_CCD007_CITY_STACKING_010); + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, mia.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, mia.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, mia.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); + + // act + + // execute two yes votes, one no vote + const votingBlock = chain.mineBlock([ccip020GracefulProtocolShutdown.voteOnProposal(user1, true), ccip020GracefulProtocolShutdown.voteOnProposal(user2, true), ccip020GracefulProtocolShutdown.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("AFTER REVERSAL"); + // console.log(`voting block reversed:\n${JSON.stringify(votingBlock, null, 2)}`); + // printVotingData(ccd007CityStacking, ccip020GracefulProtocolShutdown); + + // vote totals MIA + ccip020GracefulProtocolShutdown + .getVoteTotalMia() + .result.expectSome() + .expectTuple({ totalAmountNo: types.uint(0), totalAmountYes: types.uint(1335), totalVotesNo: types.uint(0), totalVotesYes: types.uint(3) }); + // vote totals NYC + ccip020GracefulProtocolShutdown + .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) + ccip020GracefulProtocolShutdown + .getVoteTotals() + .result.expectSome() + .expectTuple({ mia: { totalAmountNo: types.uint(0), totalAmountYes: types.uint(1335), totalVotesNo: types.uint(0), totalVotesYes: types.uint(3) }, 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 + ccip020GracefulProtocolShutdown + .getVoterInfo(1) + .result.expectSome() + .expectTuple({ mia: types.uint(445), nyc: types.uint(500), vote: types.bool(true) }); + ccd007CityStacking.getStacker(mia.cityId, 2, 1).result.expectTuple({ claimable: types.uint(0), stacked: types.uint(500) }); + ccip020GracefulProtocolShutdown.getMiaVote(1, false).result.expectSome().expectUint(445); + ccd007CityStacking.getStacker(nyc.cityId, 2, 1).result.expectTuple({ claimable: types.uint(0), stacked: types.uint(500) }); + ccip020GracefulProtocolShutdown.getNycVote(1, false).result.expectSome().expectUint(500); + + // user 2 stats + ccip020GracefulProtocolShutdown + .getVoterInfo(2) + .result.expectSome() + .expectTuple({ mia: types.uint(445), nyc: types.uint(500), vote: types.bool(true) }); + ccd007CityStacking.getStacker(mia.cityId, 2, 2).result.expectTuple({ claimable: types.uint(0), stacked: types.uint(500) }); + ccip020GracefulProtocolShutdown.getMiaVote(2, false).result.expectSome().expectUint(445); + ccd007CityStacking.getStacker(nyc.cityId, 2, 2).result.expectTuple({ claimable: types.uint(0), stacked: types.uint(500) }); + ccip020GracefulProtocolShutdown.getNycVote(2, false).result.expectSome().expectUint(500); + + // user 3 stats + ccip020GracefulProtocolShutdown + .getVoterInfo(3) + .result.expectSome() + .expectTuple({ mia: types.uint(445), nyc: types.uint(500), vote: types.bool(false) }); + ccd007CityStacking.getStacker(mia.cityId, 2, 3).result.expectTuple({ claimable: types.uint(0), stacked: types.uint(500) }); + ccip020GracefulProtocolShutdown.getMiaVote(3, false).result.expectSome().expectUint(445); + ccd007CityStacking.getStacker(nyc.cityId, 2, 3).result.expectTuple({ claimable: types.uint(0), stacked: types.uint(500) }); + ccip020GracefulProtocolShutdown.getNycVote(3, false).result.expectSome().expectUint(500); + + const votingBlockReversed = chain.mineBlock([ccip020GracefulProtocolShutdown.voteOnProposal(user3, true)]); + votingBlockReversed.receipts[0].result.expectOk().expectBool(true); + + // vote totals MIA + ccip020GracefulProtocolShutdown + .getVoteTotalMia() + .result.expectSome() + .expectTuple({ totalAmountNo: types.uint(0), totalAmountYes: types.uint(1335), totalVotesNo: types.uint(0), totalVotesYes: types.uint(3) }); + // vote totals NYC + ccip020GracefulProtocolShutdown + .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) + ccip020GracefulProtocolShutdown + .getVoteTotals() + .result.expectSome() + .expectTuple({ mia: { totalAmountNo: types.uint(0), totalAmountYes: types.uint(1335), totalVotesNo: types.uint(0), totalVotesYes: types.uint(3) }, 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 + ccip020GracefulProtocolShutdown + .getVoterInfo(1) + .result.expectSome() + .expectTuple({ mia: types.uint(445), nyc: types.uint(500), vote: types.bool(true) }); + ccd007CityStacking.getStacker(mia.cityId, 2, 1).result.expectTuple({ claimable: types.uint(0), stacked: types.uint(500) }); + ccip020GracefulProtocolShutdown.getMiaVote(1, false).result.expectSome().expectUint(445); + ccd007CityStacking.getStacker(nyc.cityId, 2, 1).result.expectTuple({ claimable: types.uint(0), stacked: types.uint(500) }); + ccip020GracefulProtocolShutdown.getNycVote(1, false).result.expectSome().expectUint(500); + + // user 2 stats + ccip020GracefulProtocolShutdown + .getVoterInfo(2) + .result.expectSome() + .expectTuple({ mia: types.uint(445), nyc: types.uint(500), vote: types.bool(true) }); + ccd007CityStacking.getStacker(mia.cityId, 2, 2).result.expectTuple({ claimable: types.uint(0), stacked: types.uint(500) }); + ccip020GracefulProtocolShutdown.getMiaVote(2, false).result.expectSome().expectUint(445); + ccd007CityStacking.getStacker(nyc.cityId, 2, 2).result.expectTuple({ claimable: types.uint(0), stacked: types.uint(500) }); + ccip020GracefulProtocolShutdown.getNycVote(2, false).result.expectSome().expectUint(500); + + // user 3 stats + ccip020GracefulProtocolShutdown + .getVoterInfo(3) + .result.expectSome() + .expectTuple({ mia: types.uint(445), nyc: types.uint(500), vote: types.bool(true) }); + ccd007CityStacking.getStacker(mia.cityId, 2, 3).result.expectTuple({ claimable: types.uint(0), stacked: types.uint(500) }); + ccip020GracefulProtocolShutdown.getMiaVote(3, false).result.expectSome().expectUint(445); + ccd007CityStacking.getStacker(nyc.cityId, 2, 3).result.expectTuple({ claimable: types.uint(0), stacked: types.uint(500) }); + ccip020GracefulProtocolShutdown.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, ccip020GracefulProtocolShutdown); + + // execute ccip-020 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_020); + + // assert + block.receipts[2].result.expectOk().expectUint(3); + }, +}); + +Clarinet.test({ + name: "ccip-020: after upgrade mining/stacking disabled, mining and stacking claims work, cycle payout works as expected", + fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const user2 = accounts.get("wallet_2")!; + const user3 = accounts.get("wallet_3")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip014pox3 = new CCIP014Pox3(chain, sender); + const ccip020GracefulProtocolShutdown = new CCIP020GracefulProtocolShutdown(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_CCIP014_POX3_001); + constructBlock.receipts[0].result.expectOk().expectBool(true); + + // prepare for the CCIP (sets up coinbase amounts, thresholds, details) + const constructBlock2 = passProposal(chain, accounts, PROPOSALS.TEST_CCIP014_POX3_002); + constructBlock2.receipts[2].result.expectOk().expectUint(3); + + // mine to put funds in the mining treasury + const miningBlockBefore = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, miningEntries), ccd006CityMining.mine(sender, nyc.cityName, miningEntries)]); + for (let i = 0; i < miningBlockBefore.receipts.length; i++) { + miningBlockBefore.receipts[i].result.expectOk().expectBool(true); + } + + // stack first cycle u1, last cycle u10 + // 3 users for MIA/NYC + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, mia.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, mia.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, mia.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 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); + + // execute single yes vote on ccip-014 + const votingBlock014 = chain.mineBlock([ccip014pox3.voteOnProposal(user1, true)]); + votingBlock014.receipts[0].result.expectOk().expectBool(true); + + // execute ccip-014 + const executeBlock = passProposal(chain, accounts, PROPOSALS.CCIP_014); + executeBlock.receipts[2].result.expectOk().expectUint(3); + + const miningBlockV1 = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, miningEntries), ccd006CityMining.mine(sender, nyc.cityName, miningEntries)]); + for (let i = 0; i < miningBlockV1.receipts.length; i++) { + miningBlockV1.receipts[i].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_MINING_DISABLED); + } + + const miningBlockV2 = chain.mineBlock([ccd006CityMiningV2.mine(sender, mia.cityName, miningEntries), ccd006CityMiningV2.mine(sender, nyc.cityName, miningEntries)]); + for (let i = 0; i < miningBlockV2.receipts.length; i++) { + miningBlockV2.receipts[i].result.expectOk().expectBool(true); + } + + // execute two yes votes, one no vote + const votingBlock = chain.mineBlock([ccip020GracefulProtocolShutdown.voteOnProposal(user1, true), ccip020GracefulProtocolShutdown.voteOnProposal(user2, true), ccip020GracefulProtocolShutdown.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, ccip020GracefulProtocolShutdown); + */ + + // act + + // execute ccip-020 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_020); + block.receipts[2].result.expectOk().expectUint(3); + + // assert + + // check that mining is disabled in V1 + const miningBlockDisabled = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, miningEntries), ccd006CityMining.mine(sender, nyc.cityName, miningEntries)]); + for (let i = 0; i < miningBlockDisabled.receipts.length; i++) { + miningBlockDisabled.receipts[i].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_MINING_DISABLED); + } + + // check that mining is disabled in V2 + const miningBlockDisabledV2 = chain.mineBlock([ccd006CityMiningV2.mine(sender, mia.cityName, miningEntries), ccd006CityMiningV2.mine(sender, nyc.cityName, miningEntries)]); + for (let i = 0; i < miningBlockDisabledV2.receipts.length; i++) { + miningBlockDisabledV2.receipts[i].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_MINING_DISABLED); + } + + // fast forward so claims are valid + chain.mineEmptyBlock(CCD006_REWARD_DELAY + 1); + + // check that mining claim still works in v1 + const miningClaimV1Height = miningBlockBefore.height - 1; + const miningClaimV1 = chain.mineBlock([ccd006CityMining.claimMiningReward(sender, mia.cityName, miningClaimV1Height), ccd006CityMining.claimMiningReward(sender, nyc.cityName, miningClaimV1Height)]); + for (let i = 0; i < miningClaimV1.receipts.length; i++) { + miningClaimV1.receipts[i].result.expectOk().expectBool(true); + } + + // check that mining claim still works in v2 + const miningClaimV2Height = miningBlockV2.height - 1; + const miningClaimV2 = chain.mineBlock([ccd006CityMiningV2.claimMiningReward(sender, mia.cityName, miningClaimV2Height), ccd006CityMiningV2.claimMiningReward(sender, nyc.cityName, miningClaimV2Height)]); + for (let i = 0; i < miningClaimV2.receipts.length; i++) { + miningClaimV2.receipts[i].result.expectOk().expectBool(true); + } + + // is-cycle-paid returns true for cycles 1-4 for MIA/NYC + for (let i = 1; i <= 4; i++) { + ccd007CityStacking.isCyclePaid(mia.cityId, i).result.expectBool(true); + ccd007CityStacking.isCyclePaid(nyc.cityId, i).result.expectBool(true); + } + + // check stacking claim works for future cycles + const claimStackingRewardFuture = chain.mineBlock([ccd007CityStacking.claimStackingReward(user1, mia.cityName, 10), ccd007CityStacking.claimStackingReward(user1, nyc.cityName, 10), ccd007CityStacking.claimStackingReward(user2, mia.cityName, 10), ccd007CityStacking.claimStackingReward(user2, nyc.cityName, 10), ccd007CityStacking.claimStackingReward(user3, mia.cityName, 10), ccd007CityStacking.claimStackingReward(user3, nyc.cityName, 10)]); + for (let i = 0; i < claimStackingRewardFuture.receipts.length; i++) { + claimStackingRewardFuture.receipts[i].result.expectOk().expectBool(true); + } + + // check that cycle 5 is not paid + ccd007CityStacking.isCyclePaid(mia.cityId, 5).result.expectBool(false); + ccd007CityStacking.isCyclePaid(nyc.cityId, 5).result.expectBool(false); + + // check that stacking payout works for cycle 5 + passProposal(chain, accounts, PROPOSALS.TEST_CCIP020_GRACEFUL_PROTOCOL_SHUTDOWN_001); + + // check that stacking claim works for cycle 5 + const claimStackingRewardAfter = chain.mineBlock([ccd007CityStacking.claimStackingReward(user1, mia.cityName, 5), ccd007CityStacking.claimStackingReward(user1, nyc.cityName, 5), ccd007CityStacking.claimStackingReward(user2, mia.cityName, 5), ccd007CityStacking.claimStackingReward(user2, nyc.cityName, 5), ccd007CityStacking.claimStackingReward(user3, mia.cityName, 5), ccd007CityStacking.claimStackingReward(user3, nyc.cityName, 5)]); + for (let i = 0; i < claimStackingRewardAfter.receipts.length; i++) { + claimStackingRewardAfter.receipts[i].result.expectOk().expectBool(true); + } + }, +}); + +/* +Clarinet.test({ + name: "", + fn(chain: Chain, accounts: Map) { + // arrange + + // act + + // assert + } +}) +*/ diff --git a/utils/common.ts b/utils/common.ts index 4df0b80c..09a8b68e 100644 --- a/utils/common.ts +++ b/utils/common.ts @@ -40,6 +40,7 @@ export const PROPOSALS = { CCIP_014: ADDRESS.concat(".ccip014-pox-3"), CCIP_014_V2: ADDRESS.concat(".ccip014-pox-3-v2"), CCIP_017: ADDRESS.concat(".ccip017-extend-sunset-period"), + CCIP_020: ADDRESS.concat(".ccip020-graceful-protocol-shutdown"), CCIP_021: ADDRESS.concat(".ccip021-extend-sunset-period-2"), TEST_CCD001_DIRECT_EXECUTE_001: ADDRESS.concat(".test-ccd001-direct-execute-001"), TEST_CCD001_DIRECT_EXECUTE_002: ADDRESS.concat(".test-ccd001-direct-execute-002"), @@ -117,6 +118,7 @@ export const PROPOSALS = { TEST_CCD011_STACKING_PAYOUTS_001: ADDRESS.concat(".test-ccd011-stacking-payouts-001"), TEST_CCIP014_POX3_001: ADDRESS.concat(".test-ccip014-pox-3-001"), TEST_CCIP014_POX3_002: ADDRESS.concat(".test-ccip014-pox-3-002"), + TEST_CCIP020_GRACEFUL_PROTOCOL_SHUTDOWN_001: ADDRESS.concat(".test-ccip020-shutdown-001"), }; export const EXTERNAL = {