diff --git a/.cache/requirements/SP000000000000000000002Q6VF78.pox.clar b/.cache/requirements/SP000000000000000000002Q6VF78.pox.clar new file mode 100644 index 00000000..21e0f696 --- /dev/null +++ b/.cache/requirements/SP000000000000000000002Q6VF78.pox.clar @@ -0,0 +1,695 @@ +;; PoX mainnet constants +;; Min/max number of reward cycles uSTX can be locked for +(define-constant MIN_POX_REWARD_CYCLES u1) +(define-constant MAX_POX_REWARD_CYCLES u12) + +;; Default length of the PoX registration window, in burnchain blocks. +(define-constant PREPARE_CYCLE_LENGTH u100) + +;; Default length of the PoX reward cycle, in burnchain blocks. +(define-constant REWARD_CYCLE_LENGTH u2100) + +;; Valid values for burnchain address versions. +;; These correspond to address hash modes in Stacks 2.0. +(define-constant ADDRESS_VERSION_P2PKH 0x00) +(define-constant ADDRESS_VERSION_P2SH 0x01) +(define-constant ADDRESS_VERSION_P2WPKH 0x02) +(define-constant ADDRESS_VERSION_P2WSH 0x03) + +;; Stacking thresholds +(define-constant STACKING_THRESHOLD_25 u20000) +(define-constant STACKING_THRESHOLD_100 u5000) + +;; The .pox contract +;; Error codes +(define-constant ERR_STACKING_UNREACHABLE 255) +(define-constant ERR_STACKING_INSUFFICIENT_FUNDS 1) +(define-constant ERR_STACKING_INVALID_LOCK_PERIOD 2) +(define-constant ERR_STACKING_ALREADY_STACKED 3) +(define-constant ERR_STACKING_NO_SUCH_PRINCIPAL 4) +(define-constant ERR_STACKING_EXPIRED 5) +(define-constant ERR_STACKING_STX_LOCKED 6) +(define-constant ERR_STACKING_PERMISSION_DENIED 9) +(define-constant ERR_STACKING_THRESHOLD_NOT_MET 11) +(define-constant ERR_STACKING_POX_ADDRESS_IN_USE 12) +(define-constant ERR_STACKING_INVALID_POX_ADDRESS 13) +(define-constant ERR_STACKING_ALREADY_REJECTED 17) +(define-constant ERR_STACKING_INVALID_AMOUNT 18) +(define-constant ERR_NOT_ALLOWED 19) +(define-constant ERR_STACKING_ALREADY_DELEGATED 20) +(define-constant ERR_DELEGATION_EXPIRES_DURING_LOCK 21) +(define-constant ERR_DELEGATION_TOO_MUCH_LOCKED 22) +(define-constant ERR_DELEGATION_POX_ADDR_REQUIRED 23) +(define-constant ERR_INVALID_START_BURN_HEIGHT 24) + +;; PoX disabling threshold (a percent) +(define-constant POX_REJECTION_FRACTION u25) + +;; Data vars that store a copy of the burnchain configuration. +;; Implemented as data-vars, so that different configurations can be +;; used in e.g. test harnesses. +(define-data-var pox-prepare-cycle-length uint PREPARE_CYCLE_LENGTH) +(define-data-var pox-reward-cycle-length uint REWARD_CYCLE_LENGTH) +(define-data-var pox-rejection-fraction uint POX_REJECTION_FRACTION) +(define-data-var first-burnchain-block-height uint u0) +(define-data-var configured bool false) + +;; This function can only be called once, when it boots up +(define-public (set-burnchain-parameters (first-burn-height uint) (prepare-cycle-length uint) (reward-cycle-length uint) (rejection-fraction uint)) + (begin + (asserts! (not (var-get configured)) (err ERR_NOT_ALLOWED)) + (var-set first-burnchain-block-height first-burn-height) + (var-set pox-prepare-cycle-length prepare-cycle-length) + (var-set pox-reward-cycle-length reward-cycle-length) + (var-set pox-rejection-fraction rejection-fraction) + (var-set configured true) + (ok true)) +) + +;; The Stacking lock-up state and associated metadata. +;; Records can be inserted into this map via one of two ways: +;; * via contract-call? to the (stack-stx) method, or +;; * via a transaction in the underlying burnchain that encodes the same data. +;; In the latter case, this map will be updated by the Stacks +;; node itself, and transactions in the burnchain will take priority +;; over transactions in the Stacks chain when processing this block. +(define-map stacking-state + { stacker: principal } + { + ;; how many uSTX locked? + amount-ustx: uint, + ;; Description of the underlying burnchain address that will + ;; receive PoX'ed tokens. Translating this into an address + ;; depends on the burnchain being used. When Bitcoin is + ;; the burnchain, this gets translated into a p2pkh, p2sh, + ;; p2wpkh-p2sh, or p2wsh-p2sh UTXO, depending on the version. + pox-addr: { version: (buff 1), hashbytes: (buff 20) }, + ;; how long the uSTX are locked, in reward cycles. + lock-period: uint, + ;; reward cycle when rewards begin + first-reward-cycle: uint + } +) + +;; Delegation relationships +(define-map delegation-state + { stacker: principal } + { + amount-ustx: uint, ;; how many uSTX delegated? + delegated-to: principal, ;; who are we delegating? + until-burn-ht: (optional uint), ;; how long does the delegation last? + ;; does the delegate _need_ to use a specific + ;; pox recipient address? + pox-addr: (optional { version: (buff 1), hashbytes: (buff 20) }) + } +) + +;; allowed contract-callers +(define-map allowance-contract-callers + { sender: principal, contract-caller: principal } + { until-burn-ht: (optional uint) }) + +;; How many uSTX are stacked in a given reward cycle. +;; Updated when a new PoX address is registered, or when more STX are granted +;; to it. +(define-map reward-cycle-total-stacked + { reward-cycle: uint } + { total-ustx: uint } +) + +;; Internal map read by the Stacks node to iterate through the list of +;; PoX reward addresses on a per-reward-cycle basis. +(define-map reward-cycle-pox-address-list + { reward-cycle: uint, index: uint } + { + pox-addr: { version: (buff 1), hashbytes: (buff 20) }, + total-ustx: uint + } +) + +(define-map reward-cycle-pox-address-list-len + { reward-cycle: uint } + { len: uint } +) + +;; how much has been locked up for this address before +;; committing? +;; this map allows stackers to stack amounts < minimum +;; by paying the cost of aggregation during the commit +(define-map partial-stacked-by-cycle + { + pox-addr: { version: (buff 1), hashbytes: (buff 20) }, + reward-cycle: uint, + sender: principal + } + { stacked-amount: uint } +) + +;; Amount of uSTX that reject PoX, by reward cycle +(define-map stacking-rejection + { reward-cycle: uint } + { amount: uint } +) + +;; Who rejected in which reward cycle +(define-map stacking-rejectors + { stacker: principal, reward-cycle: uint } + { amount: uint } +) + +;; Getter for stacking-rejectors +(define-read-only (get-pox-rejection (stacker principal) (reward-cycle uint)) + (map-get? stacking-rejectors { stacker: stacker, reward-cycle: reward-cycle })) + +;; Has PoX been rejected in the given reward cycle? +(define-read-only (is-pox-active (reward-cycle uint)) + (let ( + (reject-votes + (default-to + u0 + (get amount (map-get? stacking-rejection { reward-cycle: reward-cycle })))) + ) + ;; (100 * reject-votes) / stx-liquid-supply < pox-rejection-fraction + (< (* u100 reject-votes) + (* (var-get pox-rejection-fraction) stx-liquid-supply))) +) + +;; What's the reward cycle number of the burnchain block height? +;; Will runtime-abort if height is less than the first burnchain block (this is intentional) +(define-private (burn-height-to-reward-cycle (height uint)) + (/ (- height (var-get first-burnchain-block-height)) (var-get pox-reward-cycle-length))) + +;; What's the block height at the start of a given reward cycle? +(define-private (reward-cycle-to-burn-height (cycle uint)) + (+ (var-get first-burnchain-block-height) (* cycle (var-get pox-reward-cycle-length)))) + +;; What's the current PoX reward cycle? +(define-private (current-pox-reward-cycle) + (burn-height-to-reward-cycle burn-block-height)) + +;; Get the _current_ PoX stacking principal information. If the information +;; is expired, or if there's never been such a stacker, then returns none. +(define-read-only (get-stacker-info (stacker principal)) + (match (map-get? stacking-state { stacker: stacker }) + stacking-info + (if (<= (+ (get first-reward-cycle stacking-info) (get lock-period stacking-info)) (current-pox-reward-cycle)) + ;; present, but lock has expired + none + ;; present, and lock has not expired + (some stacking-info) + ) + ;; no state at all + none + )) + +(define-private (check-caller-allowed) + (or (is-eq tx-sender contract-caller) + (let ((caller-allowed + ;; if not in the caller map, return false + (unwrap! (map-get? allowance-contract-callers + { sender: tx-sender, contract-caller: contract-caller }) + false))) + ;; is the caller allowance expired? + (if (< burn-block-height (unwrap! (get until-burn-ht caller-allowed) true)) + false + true)))) + +(define-private (get-check-delegation (stacker principal)) + (let ((delegation-info (try! (map-get? delegation-state { stacker: stacker })))) + ;; did the existing delegation expire? + (if (match (get until-burn-ht delegation-info) + until-burn-ht (> burn-block-height until-burn-ht) + false) + ;; it expired, return none + none + ;; delegation is active + (some delegation-info)))) + +;; Get the size of the reward set for a reward cycle. +;; Note that this does _not_ return duplicate PoX addresses. +;; Note that this also _will_ return PoX addresses that are beneath +;; the minimum threshold -- i.e. the threshold can increase after insertion. +;; Used internally by the Stacks node, which filters out the entries +;; in this map to select PoX addresses with enough STX. +(define-read-only (get-reward-set-size (reward-cycle uint)) + (default-to + u0 + (get len (map-get? reward-cycle-pox-address-list-len { reward-cycle: reward-cycle })))) + +;; How many rejection votes have we been accumulating for the next block +(define-private (next-cycle-rejection-votes) + (default-to + u0 + (get amount (map-get? stacking-rejection { reward-cycle: (+ u1 (current-pox-reward-cycle)) })))) + +;; Add a single PoX address to a single reward cycle. +;; Used to build up a set of per-reward-cycle PoX addresses. +;; No checking will be done -- don't call if this PoX address is already registered in this reward cycle! +(define-private (append-reward-cycle-pox-addr (pox-addr (tuple (version (buff 1)) (hashbytes (buff 20)))) + (reward-cycle uint) + (amount-ustx uint)) + (let ( + (sz (get-reward-set-size reward-cycle)) + ) + (map-set reward-cycle-pox-address-list + { reward-cycle: reward-cycle, index: sz } + { pox-addr: pox-addr, total-ustx: amount-ustx }) + (map-set reward-cycle-pox-address-list-len + { reward-cycle: reward-cycle } + { len: (+ u1 sz) }) + (+ u1 sz)) +) + +;; How many uSTX are stacked? +(define-read-only (get-total-ustx-stacked (reward-cycle uint)) + (default-to + u0 + (get total-ustx (map-get? reward-cycle-total-stacked { reward-cycle: reward-cycle }))) +) + +;; Called internally by the node to iterate through the list of PoX addresses in this reward cycle. +;; Returns (optional (tuple (pox-addr ) (total-ustx ))) +(define-read-only (get-reward-set-pox-address (reward-cycle uint) (index uint)) + (map-get? reward-cycle-pox-address-list { reward-cycle: reward-cycle, index: index })) + +;; Add a PoX address to the ith reward cycle, if i is between 0 and the given num-cycles (exclusive). +;; Arguments are given as a tuple, so this function can be (map ..)'ed onto a list of its arguments. +;; Used by add-pox-addr-to-reward-cycles. +;; No checking is done. +;; Returns 1 if added. +;; Returns 0 if not added. +(define-private (add-pox-addr-to-ith-reward-cycle (cycle-index uint) (params (tuple + (pox-addr (tuple (version (buff 1)) (hashbytes (buff 20)))) + (first-reward-cycle uint) + (num-cycles uint) + (amount-ustx uint) + (i uint)))) + (let ((reward-cycle (+ (get first-reward-cycle params) (get i params))) + (num-cycles (get num-cycles params)) + (i (get i params))) + { + pox-addr: (get pox-addr params), + first-reward-cycle: (get first-reward-cycle params), + num-cycles: num-cycles, + amount-ustx: (get amount-ustx params), + i: (if (< i num-cycles) + (let ((total-ustx (get-total-ustx-stacked reward-cycle))) + ;; record how many uSTX this pox-addr will stack for in the given reward cycle + (append-reward-cycle-pox-addr + (get pox-addr params) + reward-cycle + (get amount-ustx params)) + + ;; update running total + (map-set reward-cycle-total-stacked + { reward-cycle: reward-cycle } + { total-ustx: (+ (get amount-ustx params) total-ustx) }) + + ;; updated _this_ reward cycle + (+ i u1)) + (+ i u0)) + })) + +;; Add a PoX address to a given sequence of reward cycle lists. +;; A PoX address can be added to at most 12 consecutive cycles. +;; No checking is done. +(define-private (add-pox-addr-to-reward-cycles (pox-addr (tuple (version (buff 1)) (hashbytes (buff 20)))) + (first-reward-cycle uint) + (num-cycles uint) + (amount-ustx uint)) + (let ((cycle-indexes (list u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11))) + ;; For safety, add up the number of times (add-principal-to-ith-reward-cycle) returns 1. + ;; It _should_ be equal to num-cycles. + (asserts! + (is-eq num-cycles + (get i (fold add-pox-addr-to-ith-reward-cycle cycle-indexes + { pox-addr: pox-addr, first-reward-cycle: first-reward-cycle, num-cycles: num-cycles, amount-ustx: amount-ustx, i: u0 }))) + (err ERR_STACKING_UNREACHABLE)) + (ok true))) + +(define-private (add-pox-partial-stacked-to-ith-cycle + (cycle-index uint) + (params { pox-addr: { version: (buff 1), hashbytes: (buff 20) }, + reward-cycle: uint, + num-cycles: uint, + amount-ustx: uint })) + (let ((pox-addr (get pox-addr params)) + (num-cycles (get num-cycles params)) + (reward-cycle (get reward-cycle params)) + (amount-ustx (get amount-ustx params))) + (let ((current-amount + (default-to u0 + (get stacked-amount + (map-get? partial-stacked-by-cycle { sender: tx-sender, pox-addr: pox-addr, reward-cycle: reward-cycle }))))) + (if (>= cycle-index num-cycles) + ;; do not add to cycles >= cycle-index + false + ;; otherwise, add to the partial-stacked-by-cycle + (map-set partial-stacked-by-cycle + { sender: tx-sender, pox-addr: pox-addr, reward-cycle: reward-cycle } + { stacked-amount: (+ amount-ustx current-amount) })) + ;; produce the next params tuple + { pox-addr: pox-addr, + reward-cycle: (+ u1 reward-cycle), + num-cycles: num-cycles, + amount-ustx: amount-ustx }))) + +;; Add a PoX address to a given sequence of partial reward cycle lists. +;; A PoX address can be added to at most 12 consecutive cycles. +;; No checking is done. +(define-private (add-pox-partial-stacked (pox-addr (tuple (version (buff 1)) (hashbytes (buff 20)))) + (first-reward-cycle uint) + (num-cycles uint) + (amount-ustx uint)) + (let ((cycle-indexes (list u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11))) + (fold add-pox-partial-stacked-to-ith-cycle cycle-indexes + { pox-addr: pox-addr, reward-cycle: first-reward-cycle, num-cycles: num-cycles, amount-ustx: amount-ustx }) + true)) + +;; What is the minimum number of uSTX to be stacked in the given reward cycle? +;; Used internally by the Stacks node, and visible publicly. +(define-read-only (get-stacking-minimum) + (/ stx-liquid-supply STACKING_THRESHOLD_25)) + +;; Is the address mode valid for a PoX burn address? +(define-private (check-pox-addr-version (version (buff 1))) + (or (is-eq version ADDRESS_VERSION_P2PKH) + (is-eq version ADDRESS_VERSION_P2SH) + (is-eq version ADDRESS_VERSION_P2WPKH) + (is-eq version ADDRESS_VERSION_P2WSH))) + +;; Is the given lock period valid? +(define-private (check-pox-lock-period (lock-period uint)) + (and (>= lock-period MIN_POX_REWARD_CYCLES) + (<= lock-period MAX_POX_REWARD_CYCLES))) + +;; Evaluate if a participant can stack an amount of STX for a given period. +;; This method is designed as a read-only method so that it can be used as +;; a set of guard conditions and also as a read-only RPC call that can be +;; performed beforehand. +(define-read-only (can-stack-stx (pox-addr (tuple (version (buff 1)) (hashbytes (buff 20)))) + (amount-ustx uint) + (first-reward-cycle uint) + (num-cycles uint)) + (begin + ;; minimum uSTX must be met + (asserts! (<= (print (get-stacking-minimum)) amount-ustx) + (err ERR_STACKING_THRESHOLD_NOT_MET)) + + (minimal-can-stack-stx pox-addr amount-ustx first-reward-cycle num-cycles))) + +;; Evaluate if a participant can stack an amount of STX for a given period. +;; This method is designed as a read-only method so that it can be used as +;; a set of guard conditions and also as a read-only RPC call that can be +;; performed beforehand. +(define-read-only (minimal-can-stack-stx + (pox-addr (tuple (version (buff 1)) (hashbytes (buff 20)))) + (amount-ustx uint) + (first-reward-cycle uint) + (num-cycles uint)) + (begin + ;; amount must be valid + (asserts! (> amount-ustx u0) + (err ERR_STACKING_INVALID_AMOUNT)) + + ;; sender principal must not have rejected in this upcoming reward cycle + (asserts! (is-none (get-pox-rejection tx-sender first-reward-cycle)) + (err ERR_STACKING_ALREADY_REJECTED)) + + ;; lock period must be in acceptable range. + (asserts! (check-pox-lock-period num-cycles) + (err ERR_STACKING_INVALID_LOCK_PERIOD)) + + ;; address version must be valid + (asserts! (check-pox-addr-version (get version pox-addr)) + (err ERR_STACKING_INVALID_POX_ADDRESS)) + (ok true))) + +;; Revoke contract-caller authorization to call stacking methods +(define-public (disallow-contract-caller (caller principal)) + (begin + (asserts! (is-eq tx-sender contract-caller) + (err ERR_STACKING_PERMISSION_DENIED)) + (ok (map-delete allowance-contract-callers { sender: tx-sender, contract-caller: caller })))) + +;; Give a contract-caller authorization to call stacking methods +;; normally, stacking methods may only be invoked by _direct_ transactions +;; (i.e., the tx-sender issues a direct contract-call to the stacking methods) +;; by issuing an allowance, the tx-sender may call through the allowed contract +(define-public (allow-contract-caller (caller principal) (until-burn-ht (optional uint))) + (begin + (asserts! (is-eq tx-sender contract-caller) + (err ERR_STACKING_PERMISSION_DENIED)) + (ok (map-set allowance-contract-callers + { sender: tx-sender, contract-caller: caller } + { until-burn-ht: until-burn-ht })))) + +;; Lock up some uSTX for stacking! Note that the given amount here is in micro-STX (uSTX). +;; The STX will be locked for the given number of reward cycles (lock-period). +;; This is the self-service interface. tx-sender will be the Stacker. +;; +;; * The given stacker cannot currently be stacking. +;; * You will need the minimum uSTX threshold. This will be determined by (get-stacking-minimum) +;; at the time this method is called. +;; * You may need to increase the amount of uSTX locked up later, since the minimum uSTX threshold +;; may increase between reward cycles. +;; * The Stacker will receive rewards in the reward cycle following `start-burn-ht`. +;; Importantly, `start-burn-ht` may not be further into the future than the next reward cycle, +;; and in most cases should be set to the current burn block height. +;; +;; The tokens will unlock and be returned to the Stacker (tx-sender) automatically. +(define-public (stack-stx (amount-ustx uint) + (pox-addr (tuple (version (buff 1)) (hashbytes (buff 20)))) + (start-burn-ht uint) + (lock-period uint)) + ;; this stacker's first reward cycle is the _next_ reward cycle + (let ((first-reward-cycle (+ u1 (current-pox-reward-cycle))) + (specified-reward-cycle (+ u1 (burn-height-to-reward-cycle start-burn-ht)))) + ;; the start-burn-ht must result in the next reward cycle, do not allow stackers + ;; to "post-date" their `stack-stx` transaction + (asserts! (is-eq first-reward-cycle specified-reward-cycle) + (err ERR_INVALID_START_BURN_HEIGHT)) + + ;; must be called directly by the tx-sender or by an allowed contract-caller + (asserts! (check-caller-allowed) + (err ERR_STACKING_PERMISSION_DENIED)) + + ;; tx-sender principal must not be stacking + (asserts! (is-none (get-stacker-info tx-sender)) + (err ERR_STACKING_ALREADY_STACKED)) + + ;; tx-sender must not be delegating + (asserts! (is-none (get-check-delegation tx-sender)) + (err ERR_STACKING_ALREADY_DELEGATED)) + + ;; the Stacker must have sufficient unlocked funds + (asserts! (>= (stx-get-balance tx-sender) amount-ustx) + (err ERR_STACKING_INSUFFICIENT_FUNDS)) + + ;; ensure that stacking can be performed + (try! (can-stack-stx pox-addr amount-ustx first-reward-cycle lock-period)) + + ;; register the PoX address with the amount stacked + (try! (add-pox-addr-to-reward-cycles pox-addr first-reward-cycle lock-period amount-ustx)) + + ;; add stacker record + (map-set stacking-state + { stacker: tx-sender } + { amount-ustx: amount-ustx, + pox-addr: pox-addr, + first-reward-cycle: first-reward-cycle, + lock-period: lock-period }) + + ;; return the lock-up information, so the node can actually carry out the lock. + (ok { stacker: tx-sender, lock-amount: amount-ustx, unlock-burn-height: (reward-cycle-to-burn-height (+ first-reward-cycle lock-period)) })) +) + +(define-public (revoke-delegate-stx) + (begin + ;; must be called directly by the tx-sender or by an allowed contract-caller + (asserts! (check-caller-allowed) + (err ERR_STACKING_PERMISSION_DENIED)) + (ok (map-delete delegation-state { stacker: tx-sender })))) + +;; Delegate to `delegate-to` the ability to stack from a given address. +;; This method _does not_ lock the funds, rather, it allows the delegate +;; to issue the stacking lock. +;; The caller specifies: +;; * amount-ustx: the total amount of ustx the delegate may be allowed to lock +;; * until-burn-ht: an optional burn height at which this delegation expiration +;; * pox-addr: an optional address to which any rewards *must* be sent +(define-public (delegate-stx (amount-ustx uint) + (delegate-to principal) + (until-burn-ht (optional uint)) + (pox-addr (optional { version: (buff 1), + hashbytes: (buff 20) }))) + (begin + ;; must be called directly by the tx-sender or by an allowed contract-caller + (asserts! (check-caller-allowed) + (err ERR_STACKING_PERMISSION_DENIED)) + + ;; tx-sender principal must not be stacking + (asserts! (is-none (get-stacker-info tx-sender)) + (err ERR_STACKING_ALREADY_STACKED)) + + ;; tx-sender must not be delegating + (asserts! (is-none (get-check-delegation tx-sender)) + (err ERR_STACKING_ALREADY_DELEGATED)) + + ;; add delegation record + (map-set delegation-state + { stacker: tx-sender } + { amount-ustx: amount-ustx, + delegated-to: delegate-to, + until-burn-ht: until-burn-ht, + pox-addr: pox-addr }) + + (ok true))) + +;; Commit partially stacked STX. +;; This allows a stacker/delegate to lock fewer STX than the minimal threshold in multiple transactions, +;; so long as: 1. The pox-addr is the same. +;; 2. This "commit" transaction is called _before_ the PoX anchor block. +;; This ensures that each entry in the reward set returned to the stacks-node is greater than the threshold, +;; but does not require it be all locked up within a single transaction +(define-public (stack-aggregation-commit (pox-addr { version: (buff 1), hashbytes: (buff 20) }) + (reward-cycle uint)) + (let ((partial-stacked + ;; fetch the partial commitments + (unwrap! (map-get? partial-stacked-by-cycle { pox-addr: pox-addr, sender: tx-sender, reward-cycle: reward-cycle }) + (err ERR_STACKING_NO_SUCH_PRINCIPAL)))) + ;; must be called directly by the tx-sender or by an allowed contract-caller + (asserts! (check-caller-allowed) + (err ERR_STACKING_PERMISSION_DENIED)) + (let ((amount-ustx (get stacked-amount partial-stacked))) + (try! (can-stack-stx pox-addr amount-ustx reward-cycle u1)) + ;; add the pox addr to the reward cycle + (add-pox-addr-to-ith-reward-cycle + u0 + { pox-addr: pox-addr, + first-reward-cycle: reward-cycle, + num-cycles: u1, + amount-ustx: amount-ustx, + i: u0 }) + ;; don't update the stacking-state map, + ;; because it _already has_ this stacker's state + ;; don't lock the STX, because the STX is already locked + ;; + ;; clear the partial-stacked state + (map-delete partial-stacked-by-cycle { pox-addr: pox-addr, sender: tx-sender, reward-cycle: reward-cycle }) + (ok true)))) + +;; As a delegate, stack the given principal's STX using partial-stacked-by-cycle +;; Once the delegate has stacked > minimum, the delegate should call stack-aggregation-commit +(define-public (delegate-stack-stx (stacker principal) + (amount-ustx uint) + (pox-addr { version: (buff 1), hashbytes: (buff 20) }) + (start-burn-ht uint) + (lock-period uint)) + ;; this stacker's first reward cycle is the _next_ reward cycle + (let ((first-reward-cycle (+ u1 (current-pox-reward-cycle))) + (specified-reward-cycle (+ u1 (burn-height-to-reward-cycle start-burn-ht))) + (unlock-burn-height (reward-cycle-to-burn-height (+ (current-pox-reward-cycle) u1 lock-period)))) + ;; the start-burn-ht must result in the next reward cycle, do not allow stackers + ;; to "post-date" their `stack-stx` transaction + (asserts! (is-eq first-reward-cycle specified-reward-cycle) + (err ERR_INVALID_START_BURN_HEIGHT)) + + ;; must be called directly by the tx-sender or by an allowed contract-caller + (asserts! (check-caller-allowed) + (err ERR_STACKING_PERMISSION_DENIED)) + + ;; stacker must have delegated to the caller + (let ((delegation-info (unwrap! (get-check-delegation stacker) (err ERR_STACKING_PERMISSION_DENIED)))) + ;; must have delegated to tx-sender + (asserts! (is-eq (get delegated-to delegation-info) tx-sender) + (err ERR_STACKING_PERMISSION_DENIED)) + ;; must have delegated enough stx + (asserts! (>= (get amount-ustx delegation-info) amount-ustx) + (err ERR_DELEGATION_TOO_MUCH_LOCKED)) + ;; if pox-addr is set, must be equal to pox-addr + (asserts! (match (get pox-addr delegation-info) + specified-pox-addr (is-eq pox-addr specified-pox-addr) + true) + (err ERR_DELEGATION_POX_ADDR_REQUIRED)) + ;; delegation must not expire before lock period + (asserts! (match (get until-burn-ht delegation-info) + until-burn-ht (>= until-burn-ht + unlock-burn-height) + true) + (err ERR_DELEGATION_EXPIRES_DURING_LOCK))) + + ;; stacker principal must not be stacking + (asserts! (is-none (get-stacker-info stacker)) + (err ERR_STACKING_ALREADY_STACKED)) + + ;; the Stacker must have sufficient unlocked funds + (asserts! (>= (stx-get-balance stacker) amount-ustx) + (err ERR_STACKING_INSUFFICIENT_FUNDS)) + + ;; ensure that stacking can be performed + (try! (minimal-can-stack-stx pox-addr amount-ustx first-reward-cycle lock-period)) + + ;; register the PoX address with the amount stacked via partial stacking + ;; before it can be included in the reward set, this must be committed! + (add-pox-partial-stacked pox-addr first-reward-cycle lock-period amount-ustx) + + ;; add stacker record + (map-set stacking-state + { stacker: stacker } + { amount-ustx: amount-ustx, + pox-addr: pox-addr, + first-reward-cycle: first-reward-cycle, + lock-period: lock-period }) + + ;; return the lock-up information, so the node can actually carry out the lock. + (ok { stacker: stacker, + lock-amount: amount-ustx, + unlock-burn-height: unlock-burn-height }))) + +;; Reject Stacking for this reward cycle. +;; tx-sender votes all its uSTX for rejection. +;; Note that unlike PoX, rejecting PoX does not lock the tx-sender's +;; tokens. PoX rejection acts like a coin vote. +(define-public (reject-pox) + (let ( + (balance (stx-get-balance tx-sender)) + (vote-reward-cycle (+ u1 (current-pox-reward-cycle))) + ) + + ;; tx-sender principal must not have rejected in this upcoming reward cycle + (asserts! (is-none (get-pox-rejection tx-sender vote-reward-cycle)) + (err ERR_STACKING_ALREADY_REJECTED)) + + ;; tx-sender can't be a stacker + (asserts! (is-none (get-stacker-info tx-sender)) + (err ERR_STACKING_ALREADY_STACKED)) + + ;; vote for rejection + (map-set stacking-rejection + { reward-cycle: vote-reward-cycle } + { amount: (+ (next-cycle-rejection-votes) balance) } + ) + + ;; mark voted + (map-set stacking-rejectors + { stacker: tx-sender, reward-cycle: vote-reward-cycle } + { amount: balance } + ) + + (ok true)) +) + +;; Used for PoX parameters discovery +(define-read-only (get-pox-info) + (ok { + min-amount-ustx: (get-stacking-minimum), + reward-cycle-id: (current-pox-reward-cycle), + prepare-cycle-length: (var-get pox-prepare-cycle-length), + first-burnchain-block-height: (var-get first-burnchain-block-height), + reward-cycle-length: (var-get pox-reward-cycle-length), + rejection-fraction: (var-get pox-rejection-fraction), + current-rejection-votes: (next-cycle-rejection-votes), + total-liquid-supply-ustx: stx-liquid-supply, + }) +) diff --git a/.cache/requirements/SP000000000000000000002Q6VF78.pox.json b/.cache/requirements/SP000000000000000000002Q6VF78.pox.json new file mode 100644 index 00000000..11162a13 --- /dev/null +++ b/.cache/requirements/SP000000000000000000002Q6VF78.pox.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch20", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-auth-v2.clar b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-auth-v2.clar new file mode 100644 index 00000000..296bdb53 --- /dev/null +++ b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-auth-v2.clar @@ -0,0 +1,630 @@ +;; MIAMICOIN AUTH CONTRACT V2 +;; CityCoins Protocol Version 2.0.0 + +(define-constant CONTRACT_OWNER tx-sender) + +;; TRAIT DEFINITIONS + +(use-trait coreTraitV2 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-core-v2-trait.citycoin-core-v2) +(use-trait tokenTraitV2 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-token-v2-trait.citycoin-token-v2) + +;; ERRORS + +(define-constant ERR_UNKNOWN_JOB (err u6000)) +(define-constant ERR_UNAUTHORIZED (err u6001)) +(define-constant ERR_JOB_IS_ACTIVE (err u6002)) +(define-constant ERR_JOB_IS_NOT_ACTIVE (err u6003)) +(define-constant ERR_ALREADY_VOTED_THIS_WAY (err u6004)) +(define-constant ERR_JOB_IS_EXECUTED (err u6005)) +(define-constant ERR_JOB_IS_NOT_APPROVED (err u6006)) +(define-constant ERR_ARGUMENT_ALREADY_EXISTS (err u6007)) +(define-constant ERR_NO_ACTIVE_CORE_CONTRACT (err u6008)) +(define-constant ERR_CORE_CONTRACT_NOT_FOUND (err u6009)) +(define-constant ERR_UNKNOWN_ARGUMENT (err u6010)) +(define-constant ERR_INCORRECT_CONTRACT_STATE (err u6011)) +(define-constant ERR_CONTRACT_ALREADY_EXISTS (err u6012)) + +;; JOB MANAGEMENT + +(define-constant REQUIRED_APPROVALS u3) + +(define-data-var lastJobId uint u0) + +(define-map Jobs + uint + { + creator: principal, + name: (string-ascii 255), + target: principal, + approvals: uint, + disapprovals: uint, + isActive: bool, + isExecuted: bool + } +) + +(define-map JobApprovers + { jobId: uint, approver: principal } + bool +) + +(define-map Approvers + principal + bool +) + +(define-map ArgumentLastIdsByType + { jobId: uint, argumentType: (string-ascii 25) } + uint +) + +(define-map UIntArgumentsByName + { jobId: uint, argumentName: (string-ascii 255) } + { argumentId: uint, value: uint} +) + +(define-map UIntArgumentsById + { jobId: uint, argumentId: uint } + { argumentName: (string-ascii 255), value: uint } +) + +(define-map PrincipalArgumentsByName + { jobId: uint, argumentName: (string-ascii 255) } + { argumentId: uint, value: principal } +) + +(define-map PrincipalArgumentsById + { jobId: uint, argumentId: uint } + { argumentName: (string-ascii 255), value: principal } +) + +;; FUNCTIONS + +(define-read-only (get-last-job-id) + (var-get lastJobId) +) + +(define-public (create-job (name (string-ascii 255)) (target principal)) + (let + ( + (newJobId (+ (var-get lastJobId) u1)) + ) + (asserts! (is-approver tx-sender) ERR_UNAUTHORIZED) + (map-set Jobs + newJobId + { + creator: tx-sender, + name: name, + target: target, + approvals: u0, + disapprovals: u0, + isActive: false, + isExecuted: false + } + ) + (var-set lastJobId newJobId) + (ok newJobId) + ) +) + +(define-read-only (get-job (jobId uint)) + (map-get? Jobs jobId) +) + +(define-public (activate-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + ) + (asserts! (is-eq (get creator job) tx-sender) ERR_UNAUTHORIZED) + (asserts! (not (get isActive job)) ERR_JOB_IS_ACTIVE) + (map-set Jobs + jobId + (merge job { isActive: true }) + ) + (ok true) + ) +) + +(define-public (approve-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + (previousVote (map-get? JobApprovers { jobId: jobId, approver: tx-sender })) + ) + (asserts! (get isActive job) ERR_JOB_IS_NOT_ACTIVE) + (asserts! (is-approver tx-sender) ERR_UNAUTHORIZED) + ;; save vote + (map-set JobApprovers + { jobId: jobId, approver: tx-sender } + true + ) + (match previousVote approved + (begin + (asserts! (not approved) ERR_ALREADY_VOTED_THIS_WAY) + (map-set Jobs jobId + (merge job + { + approvals: (+ (get approvals job) u1), + disapprovals: (- (get disapprovals job) u1) + } + ) + ) + ) + ;; no previous vote + (map-set Jobs + jobId + (merge job { approvals: (+ (get approvals job) u1) } ) + ) + ) + (ok true) + ) +) + +(define-public (disapprove-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + (previousVote (map-get? JobApprovers { jobId: jobId, approver: tx-sender })) + ) + (asserts! (get isActive job) ERR_JOB_IS_NOT_ACTIVE) + (asserts! (is-approver tx-sender) ERR_UNAUTHORIZED) + ;; save vote + (map-set JobApprovers + { jobId: jobId, approver: tx-sender } + false + ) + (match previousVote approved + (begin + (asserts! approved ERR_ALREADY_VOTED_THIS_WAY) + (map-set Jobs jobId + (merge job + { + approvals: (- (get approvals job) u1), + disapprovals: (+ (get disapprovals job) u1) + } + ) + ) + ) + ;; no previous vote + (map-set Jobs + jobId + (merge job { disapprovals: (+ (get disapprovals job) u1) } ) + ) + ) + (ok true) + ) +) + +(define-read-only (is-job-approved (jobId uint)) + (match (get-job jobId) job + (>= (get approvals job) REQUIRED_APPROVALS) + false + ) +) + +(define-public (mark-job-as-executed (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + ) + (asserts! (get isActive job) ERR_JOB_IS_NOT_ACTIVE) + (asserts! (>= (get approvals job) REQUIRED_APPROVALS) ERR_JOB_IS_NOT_APPROVED) + (asserts! (is-eq (get target job) contract-caller) ERR_UNAUTHORIZED) + (asserts! (not (get isExecuted job)) ERR_JOB_IS_EXECUTED) + (map-set Jobs + jobId + (merge job { isExecuted: true }) + ) + (ok true) + ) +) + +(define-public (add-uint-argument (jobId uint) (argumentName (string-ascii 255)) (value uint)) + (let + ( + (argumentId (generate-argument-id jobId "uint")) + ) + (try! (guard-add-argument jobId)) + (asserts! + (and + (map-insert UIntArgumentsById + { jobId: jobId, argumentId: argumentId } + { argumentName: argumentName, value: value } + ) + (map-insert UIntArgumentsByName + { jobId: jobId, argumentName: argumentName } + { argumentId: argumentId, value: value} + ) + ) + ERR_ARGUMENT_ALREADY_EXISTS + ) + (ok true) + ) +) + +(define-read-only (get-uint-argument-by-name (jobId uint) (argumentName (string-ascii 255))) + (map-get? UIntArgumentsByName { jobId: jobId, argumentName: argumentName }) +) + +(define-read-only (get-uint-argument-by-id (jobId uint) (argumentId uint)) + (map-get? UIntArgumentsById { jobId: jobId, argumentId: argumentId }) +) + +(define-read-only (get-uint-value-by-name (jobId uint) (argumentName (string-ascii 255))) + (get value (get-uint-argument-by-name jobId argumentName)) +) + +(define-read-only (get-uint-value-by-id (jobId uint) (argumentId uint)) + (get value (get-uint-argument-by-id jobId argumentId)) +) + +(define-public (add-principal-argument (jobId uint) (argumentName (string-ascii 255)) (value principal)) + (let + ( + (argumentId (generate-argument-id jobId "principal")) + ) + (try! (guard-add-argument jobId)) + (asserts! + (and + (map-insert PrincipalArgumentsById + { jobId: jobId, argumentId: argumentId } + { argumentName: argumentName, value: value } + ) + (map-insert PrincipalArgumentsByName + { jobId: jobId, argumentName: argumentName } + { argumentId: argumentId, value: value} + ) + ) + ERR_ARGUMENT_ALREADY_EXISTS + ) + (ok true) + ) +) + +(define-read-only (get-principal-argument-by-name (jobId uint) (argumentName (string-ascii 255))) + (map-get? PrincipalArgumentsByName { jobId: jobId, argumentName: argumentName }) +) + +(define-read-only (get-principal-argument-by-id (jobId uint) (argumentId uint)) + (map-get? PrincipalArgumentsById { jobId: jobId, argumentId: argumentId }) +) + +(define-read-only (get-principal-value-by-name (jobId uint) (argumentName (string-ascii 255))) + (get value (get-principal-argument-by-name jobId argumentName)) +) + +(define-read-only (get-principal-value-by-id (jobId uint) (argumentId uint)) + (get value (get-principal-argument-by-id jobId argumentId)) +) + +;; PRIVATE FUNCTIONS + +(define-read-only (is-approver (user principal)) + (default-to false (map-get? Approvers user)) +) + +(define-private (generate-argument-id (jobId uint) (argumentType (string-ascii 25))) + (let + ( + (argumentId (+ (default-to u0 (map-get? ArgumentLastIdsByType { jobId: jobId, argumentType: argumentType })) u1)) + ) + (map-set ArgumentLastIdsByType + { jobId: jobId, argumentType: argumentType } + argumentId + ) + ;; return + argumentId + ) +) + +(define-private (guard-add-argument (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + ) + (asserts! (not (get isActive job)) ERR_JOB_IS_ACTIVE) + (asserts! (is-eq (get creator job) contract-caller) ERR_UNAUTHORIZED) + (ok true) + ) +) + +;; CONTRACT MANAGEMENT + +;; initial value for active core contract +;; set to deployer address at startup to prevent +;; circular dependency of core on auth +(define-data-var activeCoreContract principal CONTRACT_OWNER) +(define-data-var initialized bool false) + +;; core contract states +(define-constant STATE_DEPLOYED u0) +(define-constant STATE_ACTIVE u1) +(define-constant STATE_INACTIVE u2) + +;; core contract map +(define-map CoreContracts + principal + { + state: uint, + startHeight: uint, + endHeight: uint + } +) + +;; getter for active core contract +(define-read-only (get-active-core-contract) + (begin + (asserts! (not (is-eq (var-get activeCoreContract) CONTRACT_OWNER)) ERR_NO_ACTIVE_CORE_CONTRACT) + (ok (var-get activeCoreContract)) + ) +) + +;; getter for core contract map +(define-read-only (get-core-contract-info (targetContract principal)) + (let + ( + (coreContract (unwrap! (map-get? CoreContracts targetContract) ERR_CORE_CONTRACT_NOT_FOUND)) + ) + (ok coreContract) + ) +) + +;; one-time function to initialize contracts after all contracts are deployed +;; - check that deployer is calling this function +;; - check this contract is not activated already (one-time use) +;; - set initial map value for core contract v1 +;; - set cityWallet in core contract +;; - set intialized true +(define-public (initialize-contracts (coreContract )) + (let + ( + (coreContractAddress (contract-of coreContract)) + ) + (asserts! (is-eq contract-caller CONTRACT_OWNER) ERR_UNAUTHORIZED) + (asserts! (not (var-get initialized)) ERR_UNAUTHORIZED) + (map-set CoreContracts + coreContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (try! (contract-call? coreContract set-city-wallet (var-get cityWallet))) + (var-set initialized true) + (ok true) + ) +) + +(define-read-only (is-initialized) + (var-get initialized) +) + +;; function to activate core contract through registration +;; - check that target is in core contract map +;; - check that caller is core contract +;; - check that target is in STATE_DEPLOYED +;; - set active in core contract map +;; - set as activeCoreContract +(define-public (activate-core-contract (targetContract principal) (stacksHeight uint)) + (let + ( + (coreContract (unwrap! (map-get? CoreContracts targetContract) ERR_CORE_CONTRACT_NOT_FOUND)) + ) + (asserts! (is-eq (get state coreContract) STATE_DEPLOYED) ERR_INCORRECT_CONTRACT_STATE) + (asserts! (is-eq contract-caller targetContract) ERR_UNAUTHORIZED) + (map-set CoreContracts + targetContract + { + state: STATE_ACTIVE, + startHeight: stacksHeight, + endHeight: u0 + }) + (var-set activeCoreContract targetContract) + (ok true) + ) +) + +;; protected function to update core contract +(define-public (upgrade-core-contract (oldContract ) (newContract )) + (let + ( + (oldContractAddress (contract-of oldContract)) + (oldContractMap (unwrap! (map-get? CoreContracts oldContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + (newContractAddress (contract-of newContract)) + ) + (asserts! (not (is-eq oldContractAddress newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (is-none (map-get? CoreContracts newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + (map-set CoreContracts + oldContractAddress + { + state: STATE_INACTIVE, + startHeight: (get startHeight oldContractMap), + endHeight: block-height + }) + (map-set CoreContracts + newContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (var-set activeCoreContract newContractAddress) + (try! (contract-call? oldContract shutdown-contract block-height)) + (try! (contract-call? newContract set-city-wallet (var-get cityWallet))) + (ok true) + ) +) + +(define-public (execute-upgrade-core-contract-job (jobId uint) (oldContract ) (newContract )) + (let + ( + (oldContractArg (unwrap! (get-principal-value-by-name jobId "oldContract") ERR_UNKNOWN_ARGUMENT)) + (newContractArg (unwrap! (get-principal-value-by-name jobId "newContract") ERR_UNKNOWN_ARGUMENT)) + (oldContractAddress (contract-of oldContract)) + (oldContractMap (unwrap! (map-get? CoreContracts oldContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + (newContractAddress (contract-of newContract)) + ) + (asserts! (not (is-eq oldContractAddress newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (is-none (map-get? CoreContracts newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (and (is-eq oldContractArg oldContractAddress) (is-eq newContractArg newContractAddress)) ERR_UNAUTHORIZED) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (map-set CoreContracts + oldContractAddress + { + state: STATE_INACTIVE, + startHeight: (get startHeight oldContractMap), + endHeight: block-height + }) + (map-set CoreContracts + newContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (var-set activeCoreContract newContractAddress) + (try! (contract-call? oldContract shutdown-contract block-height)) + (try! (contract-call? newContract set-city-wallet (var-get cityWallet))) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; CITY WALLET MANAGEMENT + +;; initial value for city wallet +(define-data-var cityWallet principal 'SM2MARAVW6BEJCD13YV2RHGYHQWT7TDDNMNRB1MVT) + +;; returns city wallet principal +(define-read-only (get-city-wallet) + (ok (var-get cityWallet)) +) + +;; protected function to update city wallet variable +(define-public (set-city-wallet (targetContract ) (newCityWallet principal)) + (let + ( + (coreContractAddress (contract-of targetContract)) + (coreContract (unwrap! (map-get? CoreContracts coreContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + ) + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + (asserts! (is-eq coreContractAddress (var-get activeCoreContract)) ERR_UNAUTHORIZED) + (var-set cityWallet newCityWallet) + (try! (contract-call? targetContract set-city-wallet newCityWallet)) + (ok true) + ) +) + +(define-public (execute-set-city-wallet-job (jobId uint) (targetContract )) + (let + ( + (coreContractAddress (contract-of targetContract)) + (coreContract (unwrap! (map-get? CoreContracts coreContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + (newCityWallet (unwrap! (get-principal-value-by-name jobId "newCityWallet") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (asserts! (is-eq coreContractAddress (var-get activeCoreContract)) ERR_UNAUTHORIZED) + (var-set cityWallet newCityWallet) + (try! (contract-call? targetContract set-city-wallet newCityWallet)) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; check if contract caller is city wallet +(define-private (is-authorized-city) + (is-eq contract-caller (var-get cityWallet)) +) + +;; TOKEN MANAGEMENT + +(define-public (set-token-uri (targetContract ) (newUri (optional (string-utf8 256)))) + (begin + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + (as-contract (try! (contract-call? targetContract set-token-uri newUri))) + (ok true) + ) +) + +;; COINBASE THRESHOLDS + +(define-public (update-coinbase-thresholds (targetCore ) (targetToken ) (threshold1 uint) (threshold2 uint) (threshold3 uint) (threshold4 uint) (threshold5 uint)) + (begin + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + ;; update in token contract + (as-contract (try! (contract-call? targetToken update-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5))) + ;; update core contract based on token contract + (as-contract (try! (contract-call? targetCore update-coinbase-thresholds))) + (ok true) + ) +) + +(define-public (execute-update-coinbase-thresholds-job (jobId uint) (targetCore ) (targetToken )) + (let + ( + (threshold1 (unwrap! (get-uint-value-by-name jobId "threshold1") ERR_UNKNOWN_ARGUMENT)) + (threshold2 (unwrap! (get-uint-value-by-name jobId "threshold2") ERR_UNKNOWN_ARGUMENT)) + (threshold3 (unwrap! (get-uint-value-by-name jobId "threshold3") ERR_UNKNOWN_ARGUMENT)) + (threshold4 (unwrap! (get-uint-value-by-name jobId "threshold4") ERR_UNKNOWN_ARGUMENT)) + (threshold5 (unwrap! (get-uint-value-by-name jobId "threshold5") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (as-contract (try! (contract-call? targetToken update-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5))) + (as-contract (try! (contract-call? targetCore update-coinbase-thresholds))) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; COINBASE AMOUNTS (REWARDS) + +(define-public (update-coinbase-amounts (targetCore ) (targetToken ) (amountBonus uint) (amount1 uint) (amount2 uint) (amount3 uint) (amount4 uint) (amount5 uint) (amountDefault uint)) + (begin + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + ;; update in token contract + (as-contract (try! (contract-call? targetToken update-coinbase-amounts amountBonus amount1 amount2 amount3 amount4 amount5 amountDefault))) + ;; update core contract based on token contract + (as-contract (try! (contract-call? targetCore update-coinbase-amounts))) + (ok true) + ) +) + +(define-public (execute-update-coinbase-amounts-job (jobId uint) (targetCore ) (targetToken )) + (let + ( + (amountBonus (unwrap! (get-uint-value-by-name jobId "amountBonus") ERR_UNKNOWN_ARGUMENT)) + (amount1 (unwrap! (get-uint-value-by-name jobId "amount1") ERR_UNKNOWN_ARGUMENT)) + (amount2 (unwrap! (get-uint-value-by-name jobId "amount2") ERR_UNKNOWN_ARGUMENT)) + (amount3 (unwrap! (get-uint-value-by-name jobId "amount3") ERR_UNKNOWN_ARGUMENT)) + (amount4 (unwrap! (get-uint-value-by-name jobId "amount4") ERR_UNKNOWN_ARGUMENT)) + (amount5 (unwrap! (get-uint-value-by-name jobId "amount5") ERR_UNKNOWN_ARGUMENT)) + (amountDefault (unwrap! (get-uint-value-by-name jobId "amountDefault") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (as-contract (try! (contract-call? targetToken update-coinbase-amounts amountBonus amount1 amount2 amount3 amount4 amount5 amountDefault))) + (as-contract (try! (contract-call? targetCore update-coinbase-amounts))) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; APPROVERS MANAGEMENT + +(define-public (execute-replace-approver-job (jobId uint)) + (let + ( + (oldApprover (unwrap! (get-principal-value-by-name jobId "oldApprover") ERR_UNKNOWN_ARGUMENT)) + (newApprover (unwrap! (get-principal-value-by-name jobId "newApprover") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (map-set Approvers oldApprover false) + (map-set Approvers newApprover true) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; CONTRACT INITIALIZATION + +(map-insert Approvers 'SP372JVX6EWE2M0XPA84MWZYRRG2M6CAC4VVC12V1 true) +(map-insert Approvers 'SP2R0DQYR7XHD161SH2GK49QRP1YSV7HE9JSG7W6G true) +(map-insert Approvers 'SPN4Y5QPGQA8882ZXW90ADC2DHYXMSTN8VAR8C3X true) +(map-insert Approvers 'SP3YYGCGX1B62CYAH4QX7PQE63YXG7RDTXD8BQHJQ true) +(map-insert Approvers 'SP7DGES13508FHRWS1FB0J3SZA326FP6QRMB6JDE true) diff --git a/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-auth-v2.json b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-auth-v2.json new file mode 100644 index 00000000..89c8a70a --- /dev/null +++ b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-auth-v2.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v1-patch.clar b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v1-patch.clar new file mode 100644 index 00000000..f97ec9aa --- /dev/null +++ b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v1-patch.clar @@ -0,0 +1,67 @@ +;; MIAMICOIN CORE CONTRACT V1 PATCH +;; CityCoins Protocol Version 2.0.0 + +(impl-trait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.citycoin-core) + +;; uses same and skips errors already defined in miamicoin-core-v1 +(define-constant ERR_UNAUTHORIZED (err u1001)) +;; generic error used to disable all functions below +(define-constant ERR_CONTRACT_DISABLED (err u1021)) + +;; DISABLED FUNCTIONS + +(define-public (register-user (memo (optional (string-utf8 50)))) + ERR_CONTRACT_DISABLED +) + +(define-public (mine-tokens (amountUstx uint) (memo (optional (buff 34)))) + ERR_CONTRACT_DISABLED +) + +(define-public (claim-mining-reward (minerBlockHeight uint)) + ERR_CONTRACT_DISABLED +) + +(define-public (stack-tokens (amountTokens uint) (lockPeriod uint)) + ERR_CONTRACT_DISABLED +) + +(define-public (claim-stacking-reward (targetCycle uint)) + ERR_CONTRACT_DISABLED +) + +(define-public (shutdown-contract (stacksHeight uint)) + ERR_CONTRACT_DISABLED +) + +;; need to allow function to succeed one time in order to be updated +;; as the new V1 core contract, then will fail after that +(define-data-var upgraded bool false) + +(define-public (set-city-wallet (newCityWallet principal)) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (if (var-get upgraded) + ;; if true + ERR_CONTRACT_DISABLED + ;; if false + (ok (var-set upgraded true)) + ) + ) +) + +;; checks if caller is auth contract +(define-private (is-authorized-auth) + (is-eq contract-caller 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-auth) +) + +;; V1 TO V2 CONVERSION + +;; pass-through function to allow burning MIA v1 +(define-public (burn-mia-v1 (amount uint) (owner principal)) + (begin + (asserts! (is-eq tx-sender owner) ERR_UNAUTHORIZED) + (as-contract (try! (contract-call? 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-token burn amount owner))) + (ok true) + ) +) diff --git a/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v1-patch.json b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v1-patch.json new file mode 100644 index 00000000..89c8a70a --- /dev/null +++ b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v1-patch.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v2.clar b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v2.clar new file mode 100644 index 00000000..f73fa55a --- /dev/null +++ b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v2.clar @@ -0,0 +1,1008 @@ +;; MIAMICOIN CORE CONTRACT V2 +;; CityCoins Protocol Version 2.0.0 + +;; GENERAL CONFIGURATION + +(impl-trait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.citycoin-core) +(impl-trait 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-core-v2-trait.citycoin-core-v2) +(define-constant CONTRACT_OWNER tx-sender) + +;; ERROR CODES + +(define-constant ERR_UNAUTHORIZED (err u1000)) +(define-constant ERR_USER_ALREADY_REGISTERED (err u1001)) +(define-constant ERR_USER_NOT_FOUND (err u1002)) +(define-constant ERR_USER_ID_NOT_FOUND (err u1003)) +(define-constant ERR_ACTIVATION_THRESHOLD_REACHED (err u1004)) +(define-constant ERR_CONTRACT_NOT_ACTIVATED (err u1005)) +(define-constant ERR_USER_ALREADY_MINED (err u1006)) +(define-constant ERR_INSUFFICIENT_COMMITMENT (err u1007)) +(define-constant ERR_INSUFFICIENT_BALANCE (err u1008)) +(define-constant ERR_USER_DID_NOT_MINE_IN_BLOCK (err u1009)) +(define-constant ERR_CLAIMED_BEFORE_MATURITY (err u1010)) +(define-constant ERR_NO_MINERS_AT_BLOCK (err u1011)) +(define-constant ERR_REWARD_ALREADY_CLAIMED (err u1012)) +(define-constant ERR_MINER_DID_NOT_WIN (err u1013)) +(define-constant ERR_NO_VRF_SEED_FOUND (err u1014)) +(define-constant ERR_STACKING_NOT_AVAILABLE (err u1015)) +(define-constant ERR_CANNOT_STACK (err u1016)) +(define-constant ERR_REWARD_CYCLE_NOT_COMPLETED (err u1017)) +(define-constant ERR_NOTHING_TO_REDEEM (err u1018)) +(define-constant ERR_UNABLE_TO_FIND_CITY_WALLET (err u1019)) +(define-constant ERR_CLAIM_IN_WRONG_CONTRACT (err u1020)) +(define-constant ERR_BLOCK_HEIGHT_IN_PAST (err u1021)) +(define-constant ERR_COINBASE_AMOUNTS_NOT_FOUND (err u1022)) + +;; CITY WALLET MANAGEMENT + +;; initial value for city wallet, set to this contract until initialized +(define-data-var cityWallet principal 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v2) + +;; returns set city wallet principal +(define-read-only (get-city-wallet) + (var-get cityWallet) +) + +;; protected function to update city wallet variable +(define-public (set-city-wallet (newCityWallet principal)) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (ok (var-set cityWallet newCityWallet)) + ) +) + +;; REGISTRATION + +(define-constant MIAMICOIN_ACTIVATION_HEIGHT u24497) +(define-data-var activationBlock uint u340282366920938463463374607431768211455) +(define-data-var activationDelay uint u0) +(define-data-var activationReached bool false) +(define-data-var activationTarget uint u0) +(define-data-var activationThreshold uint u20) +(define-data-var usersNonce uint u0) + +;; returns Stacks block height registration was activated at plus activationDelay +(define-read-only (get-activation-block) + (begin + (asserts! (get-activation-status) ERR_CONTRACT_NOT_ACTIVATED) + (ok (var-get activationBlock)) + ) +) + +;; returns activation delay +(define-read-only (get-activation-delay) + (var-get activationDelay) +) + +;; returns activation status as boolean +(define-read-only (get-activation-status) + (var-get activationReached) +) + +;; returns activation target +(define-read-only (get-activation-target) + (begin + (asserts! (get-activation-status) ERR_CONTRACT_NOT_ACTIVATED) + (ok (var-get activationTarget)) + ) +) + +;; returns activation threshold +(define-read-only (get-activation-threshold) + (var-get activationThreshold) +) + +;; returns number of registered users, used for activation and tracking user IDs +(define-read-only (get-registered-users-nonce) + (var-get usersNonce) +) + +;; store user principal by user id +(define-map Users + uint + principal +) + +;; store user id by user principal +(define-map UserIds + principal + uint +) + +;; returns (some userId) or none +(define-read-only (get-user-id (user principal)) + (map-get? UserIds user) +) + +;; returns (some userPrincipal) or none +(define-read-only (get-user (userId uint)) + (map-get? Users userId) +) + +;; returns user ID if it has been created, or creates and returns new ID +(define-private (get-or-create-user-id (user principal)) + (match + (map-get? UserIds user) + value value + (let + ( + (newId (+ u1 (var-get usersNonce))) + ) + (map-set Users newId user) + (map-set UserIds user newId) + (var-set usersNonce newId) + newId + ) + ) +) + +;; registers users that signal activation of contract until threshold is met +(define-public (register-user (memo (optional (string-utf8 50)))) + (let + ( + (newId (+ u1 (var-get usersNonce))) + (threshold (var-get activationThreshold)) + (initialized (contract-call? 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-auth-v2 is-initialized)) + ) + + (asserts! initialized ERR_UNAUTHORIZED) + + (asserts! (is-none (map-get? UserIds tx-sender)) + ERR_USER_ALREADY_REGISTERED) + + (asserts! (<= newId threshold) + ERR_ACTIVATION_THRESHOLD_REACHED) + + (if (is-some memo) + (print memo) + none + ) + + (get-or-create-user-id tx-sender) + + (if (is-eq newId threshold) + (let + ( + (activationTargetBlock (+ block-height (var-get activationDelay))) + ) + (try! (contract-call? 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-auth-v2 activate-core-contract (as-contract tx-sender) activationTargetBlock)) + (try! (contract-call? 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2 activate-token (as-contract tx-sender) MIAMICOIN_ACTIVATION_HEIGHT)) + (try! (set-coinbase-thresholds)) + (try! (set-coinbase-amounts)) + (var-set activationReached true) + (var-set activationBlock MIAMICOIN_ACTIVATION_HEIGHT) + (var-set activationTarget activationTargetBlock) + (ok true) + ) + (ok true) + ) + ) +) + +;; MINING CONFIGURATION + +;; define split to custodied wallet address for the city +(define-constant SPLIT_CITY_PCT u30) + +;; how long a miner must wait before block winner can claim their minted tokens +(define-data-var tokenRewardMaturity uint u100) + +;; At a given Stacks block height: +;; - how many miners were there +;; - what was the total amount submitted +;; - what was the total amount submitted to the city +;; - what was the total amount submitted to Stackers +;; - was the block reward claimed +(define-map MiningStatsAtBlock + uint + { + minersCount: uint, + amount: uint, + amountToCity: uint, + amountToStackers: uint, + rewardClaimed: bool + } +) + +;; returns map MiningStatsAtBlock at a given Stacks block height if it exists +(define-read-only (get-mining-stats-at-block (stacksHeight uint)) + (map-get? MiningStatsAtBlock stacksHeight) +) + +;; returns map MiningStatsAtBlock at a given Stacks block height +;; or, an empty structure +(define-read-only (get-mining-stats-at-block-or-default (stacksHeight uint)) + (default-to { + minersCount: u0, + amount: u0, + amountToCity: u0, + amountToStackers: u0, + rewardClaimed: false + } + (map-get? MiningStatsAtBlock stacksHeight) + ) +) + +;; At a given Stacks block height and user ID: +;; - what is their ustx commitment +;; - what are the low/high values (used for VRF) +(define-map MinersAtBlock + { + stacksHeight: uint, + userId: uint + } + { + ustx: uint, + lowValue: uint, + highValue: uint, + winner: bool + } +) + +;; returns true if a given miner has already mined at a given block height +(define-read-only (has-mined-at-block (stacksHeight uint) (userId uint)) + (is-some + (map-get? MinersAtBlock { stacksHeight: stacksHeight, userId: userId }) + ) +) + +;; returns map MinersAtBlock at a given Stacks block height for a user ID +(define-read-only (get-miner-at-block (stacksHeight uint) (userId uint)) + (map-get? MinersAtBlock { stacksHeight: stacksHeight, userId: userId }) +) + +;; returns map MinersAtBlock at a given Stacks block height for a user ID +;; or, an empty structure +(define-read-only (get-miner-at-block-or-default (stacksHeight uint) (userId uint)) + (default-to { + highValue: u0, + lowValue: u0, + ustx: u0, + winner: false + } + (map-get? MinersAtBlock { stacksHeight: stacksHeight, userId: userId })) +) + +;; At a given Stacks block height: +;; - what is the max highValue from MinersAtBlock (used for VRF) +(define-map MinersAtBlockHighValue + uint + uint +) + +;; returns last high value from map MinersAtBlockHighValue +(define-read-only (get-last-high-value-at-block (stacksHeight uint)) + (default-to u0 + (map-get? MinersAtBlockHighValue stacksHeight)) +) + +;; At a given Stacks block height: +;; - what is the userId of miner who won this block +(define-map BlockWinnerIds + uint + uint +) + +(define-read-only (get-block-winner-id (stacksHeight uint)) + (map-get? BlockWinnerIds stacksHeight) +) + +;; MINING ACTIONS + +(define-public (mine-tokens (amountUstx uint) (memo (optional (buff 34)))) + (let + ( + (userId (get-or-create-user-id tx-sender)) + ) + (try! (mine-tokens-at-block userId block-height amountUstx memo)) + (ok true) + ) +) + +(define-public (mine-many (amounts (list 200 uint))) + (begin + (asserts! (is-activated) ERR_CONTRACT_NOT_ACTIVATED) + (asserts! (> (len amounts) u0) ERR_INSUFFICIENT_COMMITMENT) + (match (fold mine-single amounts (ok { userId: (get-or-create-user-id tx-sender), toStackers: u0, toCity: u0, stacksHeight: block-height })) + okReturn + (begin + (asserts! (>= (stx-get-balance tx-sender) (+ (get toStackers okReturn) (get toCity okReturn))) ERR_INSUFFICIENT_BALANCE) + (if (> (get toStackers okReturn ) u0) + (try! (stx-transfer? (get toStackers okReturn ) tx-sender (as-contract tx-sender))) + false + ) + (try! (stx-transfer? (get toCity okReturn) tx-sender (var-get cityWallet))) + (print { + firstBlock: block-height, + lastBlock: (- (+ block-height (len amounts)) u1) + }) + (ok true) + ) + errReturn (err errReturn) + ) + ) +) + +(define-private (mine-single + (amountUstx uint) + (return (response + { + userId: uint, + toStackers: uint, + toCity: uint, + stacksHeight: uint + } + uint + ))) + + (match return okReturn + (let + ( + (stacksHeight (get stacksHeight okReturn)) + (rewardCycle (default-to u0 (get-reward-cycle stacksHeight))) + (stackingActive (stacking-active-at-cycle rewardCycle)) + (toCity + (if stackingActive + (/ (* SPLIT_CITY_PCT amountUstx) u100) + amountUstx + ) + ) + (toStackers (- amountUstx toCity)) + ) + (asserts! (not (has-mined-at-block stacksHeight (get userId okReturn))) ERR_USER_ALREADY_MINED) + (asserts! (> amountUstx u0) ERR_INSUFFICIENT_COMMITMENT) + (try! (set-tokens-mined (get userId okReturn) stacksHeight amountUstx toStackers toCity)) + (ok (merge okReturn + { + toStackers: (+ (get toStackers okReturn) toStackers), + toCity: (+ (get toCity okReturn) toCity), + stacksHeight: (+ stacksHeight u1) + } + )) + ) + errReturn (err errReturn) + ) +) + +(define-private (mine-tokens-at-block (userId uint) (stacksHeight uint) (amountUstx uint) (memo (optional (buff 34)))) + (let + ( + (rewardCycle (default-to u0 (get-reward-cycle stacksHeight))) + (stackingActive (stacking-active-at-cycle rewardCycle)) + (toCity + (if stackingActive + (/ (* SPLIT_CITY_PCT amountUstx) u100) + amountUstx + ) + ) + (toStackers (- amountUstx toCity)) + ) + (asserts! (is-activated) ERR_CONTRACT_NOT_ACTIVATED) + (asserts! (not (has-mined-at-block stacksHeight userId)) ERR_USER_ALREADY_MINED) + (asserts! (> amountUstx u0) ERR_INSUFFICIENT_COMMITMENT) + (asserts! (>= (stx-get-balance tx-sender) amountUstx) ERR_INSUFFICIENT_BALANCE) + (try! (set-tokens-mined userId stacksHeight amountUstx toStackers toCity)) + (if (is-some memo) + (print memo) + none + ) + (if stackingActive + (try! (stx-transfer? toStackers tx-sender (as-contract tx-sender))) + false + ) + (try! (stx-transfer? toCity tx-sender (var-get cityWallet))) + (ok true) + ) +) + +(define-private (set-tokens-mined (userId uint) (stacksHeight uint) (amountUstx uint) (toStackers uint) (toCity uint)) + (let + ( + (blockStats (get-mining-stats-at-block-or-default stacksHeight)) + (newMinersCount (+ (get minersCount blockStats) u1)) + (minerLowVal (get-last-high-value-at-block stacksHeight)) + (rewardCycle (unwrap! (get-reward-cycle stacksHeight) + ERR_STACKING_NOT_AVAILABLE)) + (rewardCycleStats (get-stacking-stats-at-cycle-or-default rewardCycle)) + ) + (map-set MiningStatsAtBlock + stacksHeight + { + minersCount: newMinersCount, + amount: (+ (get amount blockStats) amountUstx), + amountToCity: (+ (get amountToCity blockStats) toCity), + amountToStackers: (+ (get amountToStackers blockStats) toStackers), + rewardClaimed: false + } + ) + (map-set MinersAtBlock + { + stacksHeight: stacksHeight, + userId: userId + } + { + ustx: amountUstx, + lowValue: (if (> minerLowVal u0) (+ minerLowVal u1) u0), + highValue: (+ minerLowVal amountUstx), + winner: false + } + ) + (map-set MinersAtBlockHighValue + stacksHeight + (+ minerLowVal amountUstx) + ) + (if (> toStackers u0) + (map-set StackingStatsAtCycle + rewardCycle + { + amountUstx: (+ (get amountUstx rewardCycleStats) toStackers), + amountToken: (get amountToken rewardCycleStats) + } + ) + false + ) + (ok true) + ) +) + +;; MINING REWARD CLAIM ACTIONS + +;; calls function to claim mining reward in active logic contract +(define-public (claim-mining-reward (minerBlockHeight uint)) + (begin + (asserts! (or (is-eq (var-get shutdownHeight) u0) (< minerBlockHeight (var-get shutdownHeight))) ERR_CLAIM_IN_WRONG_CONTRACT) + (try! (claim-mining-reward-at-block tx-sender block-height minerBlockHeight)) + (ok true) + ) +) + +;; Determine whether or not the given principal can claim the mined tokens at a particular block height, +;; given the miners record for that block height, a random sample, and the current block height. +(define-private (claim-mining-reward-at-block (user principal) (stacksHeight uint) (minerBlockHeight uint)) + (let + ( + (maturityHeight (+ (var-get tokenRewardMaturity) minerBlockHeight)) + (userId (unwrap! (get-user-id user) ERR_USER_ID_NOT_FOUND)) + (blockStats (unwrap! (get-mining-stats-at-block minerBlockHeight) ERR_NO_MINERS_AT_BLOCK)) + (minerStats (unwrap! (get-miner-at-block minerBlockHeight userId) ERR_USER_DID_NOT_MINE_IN_BLOCK)) + (isMature (asserts! (> stacksHeight maturityHeight) ERR_CLAIMED_BEFORE_MATURITY)) + (vrfSample (unwrap! (contract-call? 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-vrf-v2 get-save-rnd maturityHeight) ERR_NO_VRF_SEED_FOUND)) + (commitTotal (get-last-high-value-at-block minerBlockHeight)) + (winningValue (mod vrfSample commitTotal)) + ) + (asserts! (not (get rewardClaimed blockStats)) ERR_REWARD_ALREADY_CLAIMED) + (asserts! (and (>= winningValue (get lowValue minerStats)) (<= winningValue (get highValue minerStats))) + ERR_MINER_DID_NOT_WIN) + (try! (set-mining-reward-claimed userId minerBlockHeight)) + (ok true) + ) +) + +(define-private (set-mining-reward-claimed (userId uint) (minerBlockHeight uint)) + (let + ( + (blockStats (get-mining-stats-at-block-or-default minerBlockHeight)) + (minerStats (get-miner-at-block-or-default minerBlockHeight userId)) + (user (unwrap! (get-user userId) ERR_USER_NOT_FOUND)) + ) + (map-set MiningStatsAtBlock + minerBlockHeight + { + minersCount: (get minersCount blockStats), + amount: (get amount blockStats), + amountToCity: (get amountToCity blockStats), + amountToStackers: (get amountToStackers blockStats), + rewardClaimed: true + } + ) + (map-set MinersAtBlock + { + stacksHeight: minerBlockHeight, + userId: userId + } + { + ustx: (get ustx minerStats), + lowValue: (get lowValue minerStats), + highValue: (get highValue minerStats), + winner: true + } + ) + (map-set BlockWinnerIds + minerBlockHeight + userId + ) + (try! (mint-coinbase user minerBlockHeight)) + (ok true) + ) +) + +(define-read-only (is-block-winner (user principal) (minerBlockHeight uint)) + (is-block-winner-and-can-claim user minerBlockHeight false) +) + +(define-read-only (can-claim-mining-reward (user principal) (minerBlockHeight uint)) + (is-block-winner-and-can-claim user minerBlockHeight true) +) + +(define-private (is-block-winner-and-can-claim (user principal) (minerBlockHeight uint) (testCanClaim bool)) + (let + ( + (userId (unwrap! (get-user-id user) false)) + (blockStats (unwrap! (get-mining-stats-at-block minerBlockHeight) false)) + (minerStats (unwrap! (get-miner-at-block minerBlockHeight userId) false)) + (maturityHeight (+ (var-get tokenRewardMaturity) minerBlockHeight)) + (vrfSample (unwrap! (contract-call? 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-vrf-v2 get-rnd maturityHeight) false)) + (commitTotal (get-last-high-value-at-block minerBlockHeight)) + (winningValue (mod vrfSample commitTotal)) + ) + (if (and (>= winningValue (get lowValue minerStats)) (<= winningValue (get highValue minerStats))) + (if testCanClaim (not (get rewardClaimed blockStats)) true) + false + ) + ) +) + +;; STACKING CONFIGURATION + +(define-constant MAX_REWARD_CYCLES u32) +(define-constant REWARD_CYCLE_INDEXES (list u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11 u12 u13 u14 u15 u16 u17 u18 u19 u20 u21 u22 u23 u24 u25 u26 u27 u28 u29 u30 u31)) + +;; how long a reward cycle is +(define-data-var rewardCycleLength uint u2100) + +;; At a given reward cycle: +;; - how many Stackers were there +;; - what is the total uSTX submitted by miners +;; - what is the total amount of tokens stacked +(define-map StackingStatsAtCycle + uint + { + amountUstx: uint, + amountToken: uint + } +) + +;; returns the total stacked tokens and committed uSTX for a given reward cycle +(define-read-only (get-stacking-stats-at-cycle (rewardCycle uint)) + (map-get? StackingStatsAtCycle rewardCycle) +) + +;; returns the total stacked tokens and committed uSTX for a given reward cycle +;; or, an empty structure +(define-read-only (get-stacking-stats-at-cycle-or-default (rewardCycle uint)) + (default-to { amountUstx: u0, amountToken: u0 } + (map-get? StackingStatsAtCycle rewardCycle)) +) + +;; At a given reward cycle and user ID: +;; - what is the total tokens Stacked? +;; - how many tokens should be returned? (based on Stacking period) +(define-map StackerAtCycle + { + rewardCycle: uint, + userId: uint + } + { + amountStacked: uint, + toReturn: uint + } +) + +(define-read-only (get-stacker-at-cycle (rewardCycle uint) (userId uint)) + (map-get? StackerAtCycle { rewardCycle: rewardCycle, userId: userId }) +) + +(define-read-only (get-stacker-at-cycle-or-default (rewardCycle uint) (userId uint)) + (default-to { amountStacked: u0, toReturn: u0 } + (map-get? StackerAtCycle { rewardCycle: rewardCycle, userId: userId })) +) + +;; get the reward cycle for a given Stacks block height +(define-read-only (get-reward-cycle (stacksHeight uint)) + (let + ( + (firstStackingBlock (var-get activationBlock)) + (rcLen (var-get rewardCycleLength)) + ) + (if (>= stacksHeight firstStackingBlock) + (some (/ (- stacksHeight firstStackingBlock) rcLen)) + none) + ) +) + +;; determine if stacking is active in a given cycle +(define-read-only (stacking-active-at-cycle (rewardCycle uint)) + (is-some + (get amountToken (map-get? StackingStatsAtCycle rewardCycle)) + ) +) + +;; get the first Stacks block height for a given reward cycle. +(define-read-only (get-first-stacks-block-in-reward-cycle (rewardCycle uint)) + (+ (var-get activationBlock) (* (var-get rewardCycleLength) rewardCycle)) +) + +;; getter for get-entitled-stacking-reward that specifies block height +(define-read-only (get-stacking-reward (userId uint) (targetCycle uint)) + (get-entitled-stacking-reward userId targetCycle block-height) +) + +;; get uSTX a Stacker can claim, given reward cycle they stacked in and current block height +;; this method only returns a positive value if: +;; - the current block height is in a subsequent reward cycle +;; - the stacker actually locked up tokens in the target reward cycle +;; - the stacker locked up _enough_ tokens to get at least one uSTX +;; it is possible to Stack tokens and not receive uSTX: +;; - if no miners commit during this reward cycle +;; - the amount stacked by user is too few that you'd be entitled to less than 1 uSTX +(define-private (get-entitled-stacking-reward (userId uint) (targetCycle uint) (stacksHeight uint)) + (let + ( + (rewardCycleStats (get-stacking-stats-at-cycle-or-default targetCycle)) + (stackerAtCycle (get-stacker-at-cycle-or-default targetCycle userId)) + (totalUstxThisCycle (get amountUstx rewardCycleStats)) + (totalStackedThisCycle (get amountToken rewardCycleStats)) + (userStackedThisCycle (get amountStacked stackerAtCycle)) + ) + (match (get-reward-cycle stacksHeight) + currentCycle + (if (and (not (var-get isShutdown)) + (or (<= currentCycle targetCycle) (is-eq u0 userStackedThisCycle))) + ;; the contract is not shut down and + ;; this cycle hasn't finished + ;; or stacker contributed nothing + u0 + ;; (totalUstxThisCycle * userStackedThisCycle) / totalStackedThisCycle + (/ (* totalUstxThisCycle userStackedThisCycle) totalStackedThisCycle) + ) + ;; before first reward cycle + u0 + ) + ) +) + +;; STACKING ACTIONS + +(define-public (stack-tokens (amountTokens uint) (lockPeriod uint)) + (let + ( + (userId (get-or-create-user-id tx-sender)) + ) + (try! (stack-tokens-at-cycle tx-sender userId amountTokens block-height lockPeriod)) + (ok true) + ) +) + +(define-private (stack-tokens-at-cycle (user principal) (userId uint) (amountTokens uint) (startHeight uint) (lockPeriod uint)) + (let + ( + (currentCycle (unwrap! (get-reward-cycle startHeight) ERR_STACKING_NOT_AVAILABLE)) + (targetCycle (+ u1 currentCycle)) + (commitment { + stackerId: userId, + amount: amountTokens, + first: targetCycle, + last: (+ targetCycle lockPeriod) + }) + ) + (asserts! (is-activated) ERR_CONTRACT_NOT_ACTIVATED) + (asserts! (and (> lockPeriod u0) (<= lockPeriod MAX_REWARD_CYCLES)) + ERR_CANNOT_STACK) + (asserts! (> amountTokens u0) ERR_CANNOT_STACK) + (try! (contract-call? 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2 transfer amountTokens tx-sender (as-contract tx-sender) none)) + (print { + firstCycle: targetCycle, + lastCycle: (- (+ targetCycle lockPeriod) u1) + }) + (match (fold stack-tokens-closure REWARD_CYCLE_INDEXES (ok commitment)) + okValue (ok true) + errValue (err errValue) + ) + ) +) + +(define-private (stack-tokens-closure (rewardCycleIdx uint) + (commitmentResponse (response + { + stackerId: uint, + amount: uint, + first: uint, + last: uint + } + uint + ))) + + (match commitmentResponse + commitment + (let + ( + (stackerId (get stackerId commitment)) + (amountToken (get amount commitment)) + (firstCycle (get first commitment)) + (lastCycle (get last commitment)) + (targetCycle (+ firstCycle rewardCycleIdx)) + ) + (begin + (if (and (>= targetCycle firstCycle) (< targetCycle lastCycle)) + (begin + (if (is-eq targetCycle (- lastCycle u1)) + (set-tokens-stacked stackerId targetCycle amountToken amountToken) + (set-tokens-stacked stackerId targetCycle amountToken u0) + ) + true + ) + false + ) + commitmentResponse + ) + ) + errValue commitmentResponse + ) +) + +(define-private (set-tokens-stacked (userId uint) (targetCycle uint) (amountStacked uint) (toReturn uint)) + (let + ( + (rewardCycleStats (get-stacking-stats-at-cycle-or-default targetCycle)) + (stackerAtCycle (get-stacker-at-cycle-or-default targetCycle userId)) + ) + (map-set StackingStatsAtCycle + targetCycle + { + amountUstx: (get amountUstx rewardCycleStats), + amountToken: (+ amountStacked (get amountToken rewardCycleStats)) + } + ) + (map-set StackerAtCycle + { + rewardCycle: targetCycle, + userId: userId + } + { + amountStacked: (+ amountStacked (get amountStacked stackerAtCycle)), + toReturn: (+ toReturn (get toReturn stackerAtCycle)) + } + ) + ) +) + +;; STACKING REWARD CLAIMS + +;; calls function to claim stacking reward in active logic contract +(define-public (claim-stacking-reward (targetCycle uint)) + (begin + (try! (claim-stacking-reward-at-cycle tx-sender block-height targetCycle)) + (ok true) + ) +) + +(define-private (claim-stacking-reward-at-cycle (user principal) (stacksHeight uint) (targetCycle uint)) + (let + ( + (currentCycle (unwrap! (get-reward-cycle stacksHeight) ERR_STACKING_NOT_AVAILABLE)) + (userId (unwrap! (get-user-id user) ERR_USER_ID_NOT_FOUND)) + (entitledUstx (get-entitled-stacking-reward userId targetCycle stacksHeight)) + (stackerAtCycle (get-stacker-at-cycle-or-default targetCycle userId)) + (toReturn (get toReturn stackerAtCycle)) + ) + (asserts! (or + (is-eq true (var-get isShutdown)) + (> currentCycle targetCycle)) + ERR_REWARD_CYCLE_NOT_COMPLETED) + (asserts! (or (> toReturn u0) (> entitledUstx u0)) ERR_NOTHING_TO_REDEEM) + ;; disable ability to claim again + (map-set StackerAtCycle + { + rewardCycle: targetCycle, + userId: userId + } + { + amountStacked: u0, + toReturn: u0 + } + ) + ;; send back tokens if user was eligible + (if (> toReturn u0) + (try! (as-contract (contract-call? 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2 transfer toReturn tx-sender user none))) + true + ) + ;; send back rewards if user was eligible + (if (> entitledUstx u0) + (try! (as-contract (stx-transfer? entitledUstx tx-sender user))) + true + ) + (ok true) + ) +) + +;; TOKEN CONFIGURATION + +;; decimals and multiplier for token +(define-constant DECIMALS u6) +(define-constant MICRO_CITYCOINS (pow u10 DECIMALS)) + +;; bonus period length for increased coinbase rewards +(define-constant TOKEN_BONUS_PERIOD u10000) + +;; coinbase thresholds per halving, used to determine halvings +(define-data-var coinbaseThreshold1 uint u0) +(define-data-var coinbaseThreshold2 uint u0) +(define-data-var coinbaseThreshold3 uint u0) +(define-data-var coinbaseThreshold4 uint u0) +(define-data-var coinbaseThreshold5 uint u0) + +;; return coinbase thresholds if contract activated +(define-read-only (get-coinbase-thresholds) + (let + ( + (activated (get-activation-status)) + ) + (asserts! activated ERR_CONTRACT_NOT_ACTIVATED) + (ok { + coinbaseThreshold1: (var-get coinbaseThreshold1), + coinbaseThreshold2: (var-get coinbaseThreshold2), + coinbaseThreshold3: (var-get coinbaseThreshold3), + coinbaseThreshold4: (var-get coinbaseThreshold4), + coinbaseThreshold5: (var-get coinbaseThreshold5) + }) + ) +) + +;; set coinbase thresholds, used during activation +(define-private (set-coinbase-thresholds) + (let + ( + (coinbaseThresholds (try! (contract-call? 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2 get-coinbase-thresholds))) + ) + (var-set coinbaseThreshold1 (get coinbaseThreshold1 coinbaseThresholds)) + (var-set coinbaseThreshold2 (get coinbaseThreshold2 coinbaseThresholds)) + (var-set coinbaseThreshold3 (get coinbaseThreshold3 coinbaseThresholds)) + (var-set coinbaseThreshold4 (get coinbaseThreshold4 coinbaseThresholds)) + (var-set coinbaseThreshold5 (get coinbaseThreshold5 coinbaseThresholds)) + ;; print coinbase thresholds + (print { + coinbaseThreshold1: (var-get coinbaseThreshold1), + coinbaseThreshold2: (var-get coinbaseThreshold2), + coinbaseThreshold3: (var-get coinbaseThreshold3), + coinbaseThreshold4: (var-get coinbaseThreshold4), + coinbaseThreshold5: (var-get coinbaseThreshold5) + }) + (ok true) + ) +) + +;; guarded function for auth to update coinbase thresholds +(define-public (update-coinbase-thresholds) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (try! (set-coinbase-thresholds)) + (ok true) + ) +) + +;; coinbase rewards per threshold, used to determine rewards +(define-data-var coinbaseAmountBonus uint u0) +(define-data-var coinbaseAmount1 uint u0) +(define-data-var coinbaseAmount2 uint u0) +(define-data-var coinbaseAmount3 uint u0) +(define-data-var coinbaseAmount4 uint u0) +(define-data-var coinbaseAmount5 uint u0) +(define-data-var coinbaseAmountDefault uint u0) + +;; return coinbase amounts if contract activated +(define-read-only (get-coinbase-amounts) + (let + ( + (activated (get-activation-status)) + ) + (asserts! activated ERR_CONTRACT_NOT_ACTIVATED) + (ok { + coinbaseAmountBonus: (var-get coinbaseAmountBonus), + coinbaseAmount1: (var-get coinbaseAmount1), + coinbaseAmount2: (var-get coinbaseAmount2), + coinbaseAmount3: (var-get coinbaseAmount3), + coinbaseAmount4: (var-get coinbaseAmount4), + coinbaseAmount5: (var-get coinbaseAmount5), + coinbaseAmountDefault: (var-get coinbaseAmountDefault) + }) + ) +) + +;; set coinbase amounts, used during activation +(define-private (set-coinbase-amounts) + (let + ( + (coinbaseAmounts (unwrap! (contract-call? 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2 get-coinbase-amounts) ERR_COINBASE_AMOUNTS_NOT_FOUND)) + ) + (var-set coinbaseAmountBonus (get coinbaseAmountBonus coinbaseAmounts)) + (var-set coinbaseAmount1 (get coinbaseAmount1 coinbaseAmounts)) + (var-set coinbaseAmount2 (get coinbaseAmount2 coinbaseAmounts)) + (var-set coinbaseAmount3 (get coinbaseAmount3 coinbaseAmounts)) + (var-set coinbaseAmount4 (get coinbaseAmount4 coinbaseAmounts)) + (var-set coinbaseAmount5 (get coinbaseAmount5 coinbaseAmounts)) + (var-set coinbaseAmountDefault (get coinbaseAmountDefault coinbaseAmounts)) + ;; print coinbase amounts + (print { + coinbaseAmountBonus: (var-get coinbaseAmountBonus), + coinbaseAmount1: (var-get coinbaseAmount1), + coinbaseAmount2: (var-get coinbaseAmount2), + coinbaseAmount3: (var-get coinbaseAmount3), + coinbaseAmount4: (var-get coinbaseAmount4), + coinbaseAmount5: (var-get coinbaseAmount5), + coinbaseAmountDefault: (var-get coinbaseAmountDefault) + }) + (ok true) + ) +) + +;; guarded function for auth to update coinbase amounts +(define-public (update-coinbase-amounts) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (try! (set-coinbase-amounts)) + (ok true) + ) +) + +;; function for deciding how many tokens to mint, depending on when they were mined +(define-read-only (get-coinbase-amount (minerBlockHeight uint)) + (begin + ;; if contract is not active, return 0 + (asserts! (>= minerBlockHeight (var-get activationBlock)) u0) + ;; if contract is active, return based on emissions schedule + ;; defined in CCIP-008 https://github.com/citycoins/governance + (asserts! (> minerBlockHeight (var-get coinbaseThreshold1)) + (if (<= (- minerBlockHeight (var-get activationBlock)) TOKEN_BONUS_PERIOD) + ;; bonus reward for initial miners + (var-get coinbaseAmountBonus) + ;; standard reward until 1st halving + (var-get coinbaseAmount1) + ) + ) + ;; computations based on each halving threshold + (asserts! (> minerBlockHeight (var-get coinbaseThreshold2)) (var-get coinbaseAmount2)) + (asserts! (> minerBlockHeight (var-get coinbaseThreshold3)) (var-get coinbaseAmount3)) + (asserts! (> minerBlockHeight (var-get coinbaseThreshold4)) (var-get coinbaseAmount4)) + (asserts! (> minerBlockHeight (var-get coinbaseThreshold5)) (var-get coinbaseAmount5)) + ;; default value after 5th halving + (var-get coinbaseAmountDefault) + ) +) + +;; mint new tokens for claimant who won at given Stacks block height +(define-private (mint-coinbase (recipient principal) (stacksHeight uint)) + (as-contract (contract-call? 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2 mint (get-coinbase-amount stacksHeight) recipient)) +) + +;; UTILITIES + +(define-data-var shutdownHeight uint u0) +(define-data-var isShutdown bool false) + +;; stop mining and stacking operations +;; in preparation for a core upgrade +(define-public (shutdown-contract (stacksHeight uint)) + (begin + ;; make sure block height is in the future + (asserts! (>= stacksHeight block-height) ERR_BLOCK_HEIGHT_IN_PAST) + ;; only allow shutdown request from AUTH + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + ;; set variables to disable mining/stacking in CORE + (var-set activationReached false) + (var-set shutdownHeight stacksHeight) + ;; set variable to allow for all stacking claims + (var-set isShutdown true) + (ok true) + ) +) + +;; checks if caller is Auth contract +(define-private (is-authorized-auth) + (is-eq contract-caller 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-auth-v2) +) + +;; checks if contract is fully activated to +;; enable mining and stacking functions +(define-private (is-activated) + (and (get-activation-status) (>= block-height (var-get activationTarget))) +) diff --git a/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v2.json b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v2.json new file mode 100644 index 00000000..89c8a70a --- /dev/null +++ b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v2.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2.clar b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2.clar new file mode 100644 index 00000000..da70bb8c --- /dev/null +++ b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2.clar @@ -0,0 +1,301 @@ +;; MIAMICOIN TOKEN V2 CONTRACT +;; CityCoins Protocol Version 2.0.0 + +;; TRAIT DEFINITIONS + +(impl-trait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-token-trait.citycoin-token) +(impl-trait 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-token-v2-trait.citycoin-token-v2) +(use-trait coreTrait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.citycoin-core) + +;; ERROR CODES + +(define-constant ERR_UNAUTHORIZED (err u2000)) +(define-constant ERR_TOKEN_NOT_ACTIVATED (err u2001)) +(define-constant ERR_TOKEN_ALREADY_ACTIVATED (err u2002)) +(define-constant ERR_V1_BALANCE_NOT_FOUND (err u2003)) +(define-constant ERR_INVALID_COINBASE_THRESHOLD (err u2004)) +(define-constant ERR_INVALID_COINBASE_AMOUNT (err u2005)) + +;; SIP-010 DEFINITION + +(impl-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait) + +(define-fungible-token miamicoin) + +(define-constant DECIMALS u6) +(define-constant MICRO_CITYCOINS (pow u10 DECIMALS)) + +;; SIP-010 FUNCTIONS + +(define-public (transfer (amount uint) (from principal) (to principal) (memo (optional (buff 34)))) + (begin + (asserts! (is-eq from tx-sender) ERR_UNAUTHORIZED) + (if (is-some memo) + (print memo) + none + ) + (ft-transfer? miamicoin amount from to) + ) +) + +(define-read-only (get-name) + (ok "miamicoin") +) + +(define-read-only (get-symbol) + (ok "MIA") +) + +(define-read-only (get-decimals) + (ok DECIMALS) +) + +(define-read-only (get-balance (user principal)) + (ok (ft-get-balance miamicoin user)) +) + +(define-read-only (get-total-supply) + (ok (ft-get-supply miamicoin)) +) + +(define-read-only (get-token-uri) + (ok (var-get tokenUri)) +) + +;; TOKEN CONFIGURATION + +;; define bonus period and initial epoch length +(define-constant TOKEN_BONUS_PERIOD u10000) +(define-constant TOKEN_EPOCH_LENGTH u25000) + +;; once activated, activation cannot happen again +(define-data-var tokenActivated bool false) + +;; core contract states +(define-constant STATE_DEPLOYED u0) +(define-constant STATE_ACTIVE u1) +(define-constant STATE_INACTIVE u2) + +;; one-time function to activate the token +(define-public (activate-token (coreContract principal) (stacksHeight uint)) + (let + ( + (coreContractMap (try! (contract-call? 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-auth-v2 get-core-contract-info coreContract))) + (threshold1 (+ stacksHeight TOKEN_BONUS_PERIOD TOKEN_EPOCH_LENGTH)) ;; 35,000 blocks + (threshold2 (+ stacksHeight TOKEN_BONUS_PERIOD (* u3 TOKEN_EPOCH_LENGTH))) ;; 85,000 blocks + (threshold3 (+ stacksHeight TOKEN_BONUS_PERIOD (* u7 TOKEN_EPOCH_LENGTH))) ;; 185,000 blocks + (threshold4 (+ stacksHeight TOKEN_BONUS_PERIOD (* u15 TOKEN_EPOCH_LENGTH))) ;; 385,000 blocks + (threshold5 (+ stacksHeight TOKEN_BONUS_PERIOD (* u31 TOKEN_EPOCH_LENGTH))) ;; 785,000 blocks + ) + (asserts! (is-eq (get state coreContractMap) STATE_ACTIVE) ERR_UNAUTHORIZED) + (asserts! (not (var-get tokenActivated)) ERR_TOKEN_ALREADY_ACTIVATED) + (var-set tokenActivated true) + (try! (set-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5)) + (ok true) + ) +) + +;; COINBASE THRESHOLDS + +;; coinbase thresholds per halving, used to select coinbase rewards in core +;; initially set by register-user in core contract per CCIP-008 +(define-data-var coinbaseThreshold1 uint u0) +(define-data-var coinbaseThreshold2 uint u0) +(define-data-var coinbaseThreshold3 uint u0) +(define-data-var coinbaseThreshold4 uint u0) +(define-data-var coinbaseThreshold5 uint u0) + +;; return coinbase thresholds if token activated +(define-read-only (get-coinbase-thresholds) + (let + ( + (activated (var-get tokenActivated)) + ) + (asserts! activated ERR_TOKEN_NOT_ACTIVATED) + (ok { + coinbaseThreshold1: (var-get coinbaseThreshold1), + coinbaseThreshold2: (var-get coinbaseThreshold2), + coinbaseThreshold3: (var-get coinbaseThreshold3), + coinbaseThreshold4: (var-get coinbaseThreshold4), + coinbaseThreshold5: (var-get coinbaseThreshold5) + }) + ) +) + +(define-private (set-coinbase-thresholds (threshold1 uint) (threshold2 uint) (threshold3 uint) (threshold4 uint) (threshold5 uint)) + (begin + ;; check that all thresholds increase in value + (asserts! (and (> threshold1 u0) (> threshold2 threshold1) (> threshold3 threshold2) (> threshold4 threshold3) (> threshold5 threshold4)) ERR_INVALID_COINBASE_THRESHOLD) + ;; set coinbase thresholds + (var-set coinbaseThreshold1 threshold1) + (var-set coinbaseThreshold2 threshold2) + (var-set coinbaseThreshold3 threshold3) + (var-set coinbaseThreshold4 threshold4) + (var-set coinbaseThreshold5 threshold5) + ;; print coinbase thresholds + (print { + coinbaseThreshold1: threshold1, + coinbaseThreshold2: threshold2, + coinbaseThreshold3: threshold3, + coinbaseThreshold4: threshold4, + coinbaseThreshold5: threshold5 + }) + (ok true) + ) +) + +;; only accessible by auth +(define-public (update-coinbase-thresholds (threshold1 uint) (threshold2 uint) (threshold3 uint) (threshold4 uint) (threshold5 uint)) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (asserts! (var-get tokenActivated) ERR_TOKEN_NOT_ACTIVATED) + (try! (set-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5)) + (ok true) + ) +) + +;; COINBASE AMOUNTS (REWARDS) + +;; coinbase rewards per threshold per CCIP-008 +(define-data-var coinbaseAmountBonus uint (* MICRO_CITYCOINS u250000)) +(define-data-var coinbaseAmount1 uint (* MICRO_CITYCOINS u100000)) +(define-data-var coinbaseAmount2 uint (* MICRO_CITYCOINS u50000)) +(define-data-var coinbaseAmount3 uint (* MICRO_CITYCOINS u25000)) +(define-data-var coinbaseAmount4 uint (* MICRO_CITYCOINS u12500)) +(define-data-var coinbaseAmount5 uint (* MICRO_CITYCOINS u6250)) +(define-data-var coinbaseAmountDefault uint (* MICRO_CITYCOINS u3125)) + +;; return coinbase thresholds if token activated +(define-read-only (get-coinbase-amounts) + (ok { + coinbaseAmountBonus: (var-get coinbaseAmountBonus), + coinbaseAmount1: (var-get coinbaseAmount1), + coinbaseAmount2: (var-get coinbaseAmount2), + coinbaseAmount3: (var-get coinbaseAmount3), + coinbaseAmount4: (var-get coinbaseAmount4), + coinbaseAmount5: (var-get coinbaseAmount5), + coinbaseAmountDefault: (var-get coinbaseAmountDefault) + }) +) + +(define-private (set-coinbase-amounts (amountBonus uint) (amount1 uint) (amount2 uint) (amount3 uint) (amount4 uint) (amount5 uint) (amountDefault uint)) + (begin + ;; check that all amounts are greater than zero + (asserts! (and (> amountBonus u0) (> amount1 u0) (> amount2 u0) (> amount3 u0) (> amount4 u0) (> amount5 u0) (> amountDefault u0)) ERR_INVALID_COINBASE_AMOUNT) + ;; set coinbase amounts in token contract + (var-set coinbaseAmountBonus amountBonus) + (var-set coinbaseAmount1 amount1) + (var-set coinbaseAmount2 amount2) + (var-set coinbaseAmount3 amount3) + (var-set coinbaseAmount4 amount4) + (var-set coinbaseAmount5 amount5) + (var-set coinbaseAmountDefault amountDefault) + ;; print coinbase amounts + (print { + coinbaseAmountBonus: amountBonus, + coinbaseAmount1: amount1, + coinbaseAmount2: amount2, + coinbaseAmount3: amount3, + coinbaseAmount4: amount4, + coinbaseAmount5: amount5, + coinbaseAmountDefault: amountDefault + }) + (ok true) + ) +) + +;; only accessible by auth +(define-public (update-coinbase-amounts (amountBonus uint) (amount1 uint) (amount2 uint) (amount3 uint) (amount4 uint) (amount5 uint) (amountDefault uint)) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + ;; (asserts! (var-get tokenActivated) ERR_TOKEN_NOT_ACTIVATED) + (try! (set-coinbase-amounts amountBonus amount1 amount2 amount3 amount4 amount5 amountDefault)) + (ok true) + ) +) + +;; V1 TO V2 CONVERSION + +(define-public (convert-to-v2) + (let + ( + (balanceV1 (unwrap! (contract-call? 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-token get-balance tx-sender) ERR_V1_BALANCE_NOT_FOUND)) + ) + ;; verify positive balance + (asserts! (> balanceV1 u0) ERR_V1_BALANCE_NOT_FOUND) + ;; burn old + (try! (contract-call? 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-core-v1-patch burn-mia-v1 balanceV1 tx-sender)) + (print { + burnedV1: balanceV1, + mintedV2: (* balanceV1 MICRO_CITYCOINS), + tx-sender: tx-sender, + contract-caller: contract-caller + }) + ;; create new + (ft-mint? miamicoin (* balanceV1 MICRO_CITYCOINS) tx-sender) + ) +) + +;; UTILITIES + +(define-data-var tokenUri (optional (string-utf8 256)) (some u"https://cdn.citycoins.co/metadata/miamicoin.json")) + +;; set token URI to new value, only accessible by Auth +(define-public (set-token-uri (newUri (optional (string-utf8 256)))) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (ok (var-set tokenUri newUri)) + ) +) + +;; mint new tokens, only accessible by a Core contract +(define-public (mint (amount uint) (recipient principal)) + (let + ( + (coreContract (try! (contract-call? 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-auth-v2 get-core-contract-info contract-caller))) + ) + (ft-mint? miamicoin amount recipient) + ) +) + +;; burn tokens +(define-public (burn (amount uint) (owner principal)) + (begin + (asserts! (is-eq tx-sender owner) ERR_UNAUTHORIZED) + (ft-burn? miamicoin amount owner) + ) +) + +;; checks if caller is Auth contract +(define-private (is-authorized-auth) + (is-eq contract-caller 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-auth-v2) +) + +;; SEND-MANY + +(define-public (send-many (recipients (list 200 { to: principal, amount: uint, memo: (optional (buff 34)) }))) + (fold check-err + (map send-token recipients) + (ok true) + ) +) + +(define-private (check-err (result (response bool uint)) (prior (response bool uint))) + (match prior ok-value + result + err-value (err err-value) + ) +) + +(define-private (send-token (recipient { to: principal, amount: uint, memo: (optional (buff 34)) })) + (send-token-with-memo (get amount recipient) (get to recipient) (get memo recipient)) +) + +(define-private (send-token-with-memo (amount uint) (to principal) (memo (optional (buff 34)))) + (let + ( + (transferOk (try! (transfer amount tx-sender to memo))) + ) + (ok transferOk) + ) +) diff --git a/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2.json b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2.json new file mode 100644 index 00000000..89c8a70a --- /dev/null +++ b/.cache/requirements/SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-auth.clar b/.cache/requirements/SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-auth.clar new file mode 100644 index 00000000..9d2790d0 --- /dev/null +++ b/.cache/requirements/SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-auth.clar @@ -0,0 +1,564 @@ +;; NEWYORKCITYCOIN AUTH CONTRACT +;; CityCoins Protocol Version 1.0.1 + +(define-constant CONTRACT_OWNER tx-sender) + +;; TRAIT DEFINITIONS + +(use-trait coreTrait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.citycoin-core) +(use-trait tokenTrait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-token-trait.citycoin-token) + +;; ERRORS + +(define-constant ERR_UNKNOWN_JOB u6000) +(define-constant ERR_UNAUTHORIZED u6001) +(define-constant ERR_JOB_IS_ACTIVE u6002) +(define-constant ERR_JOB_IS_NOT_ACTIVE u6003) +(define-constant ERR_ALREADY_VOTED_THIS_WAY u6004) +(define-constant ERR_JOB_IS_EXECUTED u6005) +(define-constant ERR_JOB_IS_NOT_APPROVED u6006) +(define-constant ERR_ARGUMENT_ALREADY_EXISTS u6007) +(define-constant ERR_NO_ACTIVE_CORE_CONTRACT u6008) +(define-constant ERR_CORE_CONTRACT_NOT_FOUND u6009) +(define-constant ERR_UNKNOWN_ARGUMENT u6010) + +;; JOB MANAGEMENT + +(define-constant REQUIRED_APPROVALS u3) + +(define-data-var lastJobId uint u0) + +(define-map Jobs + uint + { + creator: principal, + name: (string-ascii 255), + target: principal, + approvals: uint, + disapprovals: uint, + isActive: bool, + isExecuted: bool + } +) + +(define-map JobApprovers + { jobId: uint, approver: principal } + bool +) + +(define-map Approvers + principal + bool +) + +(define-map ArgumentLastIdsByType + { jobId: uint, argumentType: (string-ascii 25) } + uint +) + +(define-map UIntArgumentsByName + { jobId: uint, argumentName: (string-ascii 255) } + { argumentId: uint, value: uint} +) + +(define-map UIntArgumentsById + { jobId: uint, argumentId: uint } + { argumentName: (string-ascii 255), value: uint } +) + +(define-map PrincipalArgumentsByName + { jobId: uint, argumentName: (string-ascii 255) } + { argumentId: uint, value: principal } +) + +(define-map PrincipalArgumentsById + { jobId: uint, argumentId: uint } + { argumentName: (string-ascii 255), value: principal } +) + +;; FUNCTIONS + +(define-read-only (get-last-job-id) + (var-get lastJobId) +) + +(define-public (create-job (name (string-ascii 255)) (target principal)) + (let + ( + (newJobId (+ (var-get lastJobId) u1)) + ) + (asserts! (is-approver tx-sender) (err ERR_UNAUTHORIZED)) + (map-set Jobs + newJobId + { + creator: tx-sender, + name: name, + target: target, + approvals: u0, + disapprovals: u0, + isActive: false, + isExecuted: false + } + ) + (var-set lastJobId newJobId) + (ok newJobId) + ) +) + +(define-read-only (get-job (jobId uint)) + (map-get? Jobs jobId) +) + +(define-public (activate-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) (err ERR_UNKNOWN_JOB))) + ) + (asserts! (is-eq (get creator job) tx-sender) (err ERR_UNAUTHORIZED)) + (asserts! (not (get isActive job)) (err ERR_JOB_IS_ACTIVE)) + (map-set Jobs + jobId + (merge job { isActive: true }) + ) + (ok true) + ) +) + +(define-public (approve-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) (err ERR_UNKNOWN_JOB))) + (previousVote (map-get? JobApprovers { jobId: jobId, approver: tx-sender })) + ) + (asserts! (get isActive job) (err ERR_JOB_IS_NOT_ACTIVE)) + (asserts! (is-approver tx-sender) (err ERR_UNAUTHORIZED)) + ;; save vote + (map-set JobApprovers + { jobId: jobId, approver: tx-sender } + true + ) + (match previousVote approved + (begin + (asserts! (not approved) (err ERR_ALREADY_VOTED_THIS_WAY)) + (map-set Jobs jobId + (merge job + { + approvals: (+ (get approvals job) u1), + disapprovals: (- (get disapprovals job) u1) + } + ) + ) + ) + ;; no previous vote + (map-set Jobs + jobId + (merge job { approvals: (+ (get approvals job) u1) } ) + ) + ) + (ok true) + ) +) + +(define-public (disapprove-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) (err ERR_UNKNOWN_JOB))) + (previousVote (map-get? JobApprovers { jobId: jobId, approver: tx-sender })) + ) + (asserts! (get isActive job) (err ERR_JOB_IS_NOT_ACTIVE)) + (asserts! (is-approver tx-sender) (err ERR_UNAUTHORIZED)) + ;; save vote + (map-set JobApprovers + { jobId: jobId, approver: tx-sender } + false + ) + (match previousVote approved + (begin + (asserts! approved (err ERR_ALREADY_VOTED_THIS_WAY)) + (map-set Jobs jobId + (merge job + { + approvals: (- (get approvals job) u1), + disapprovals: (+ (get disapprovals job) u1) + } + ) + ) + ) + ;; no previous vote + (map-set Jobs + jobId + (merge job { disapprovals: (+ (get disapprovals job) u1) } ) + ) + ) + (ok true) + ) +) + +(define-read-only (is-job-approved (jobId uint)) + (match (get-job jobId) job + (>= (get approvals job) REQUIRED_APPROVALS) + false + ) +) + +(define-public (mark-job-as-executed (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) (err ERR_UNKNOWN_JOB))) + ) + (asserts! (get isActive job) (err ERR_JOB_IS_NOT_ACTIVE)) + (asserts! (>= (get approvals job) REQUIRED_APPROVALS) (err ERR_JOB_IS_NOT_APPROVED)) + (asserts! (is-eq (get target job) contract-caller) (err ERR_UNAUTHORIZED)) + (asserts! (not (get isExecuted job)) (err ERR_JOB_IS_EXECUTED)) + (map-set Jobs + jobId + (merge job { isExecuted: true }) + ) + (ok true) + ) +) + +(define-public (add-uint-argument (jobId uint) (argumentName (string-ascii 255)) (value uint)) + (let + ( + (argumentId (generate-argument-id jobId "uint")) + ) + (try! (guard-add-argument jobId)) + (asserts! + (and + (map-insert UIntArgumentsById + { jobId: jobId, argumentId: argumentId } + { argumentName: argumentName, value: value } + ) + (map-insert UIntArgumentsByName + { jobId: jobId, argumentName: argumentName } + { argumentId: argumentId, value: value} + ) + ) + (err ERR_ARGUMENT_ALREADY_EXISTS) + ) + (ok true) + ) +) + +(define-read-only (get-uint-argument-by-name (jobId uint) (argumentName (string-ascii 255))) + (map-get? UIntArgumentsByName { jobId: jobId, argumentName: argumentName }) +) + +(define-read-only (get-uint-argument-by-id (jobId uint) (argumentId uint)) + (map-get? UIntArgumentsById { jobId: jobId, argumentId: argumentId }) +) + +(define-read-only (get-uint-value-by-name (jobId uint) (argumentName (string-ascii 255))) + (get value (get-uint-argument-by-name jobId argumentName)) +) + +(define-read-only (get-uint-value-by-id (jobId uint) (argumentId uint)) + (get value (get-uint-argument-by-id jobId argumentId)) +) + +(define-public (add-principal-argument (jobId uint) (argumentName (string-ascii 255)) (value principal)) + (let + ( + (argumentId (generate-argument-id jobId "principal")) + ) + (try! (guard-add-argument jobId)) + (asserts! + (and + (map-insert PrincipalArgumentsById + { jobId: jobId, argumentId: argumentId } + { argumentName: argumentName, value: value } + ) + (map-insert PrincipalArgumentsByName + { jobId: jobId, argumentName: argumentName } + { argumentId: argumentId, value: value} + ) + ) + (err ERR_ARGUMENT_ALREADY_EXISTS) + ) + (ok true) + ) +) + +(define-read-only (get-principal-argument-by-name (jobId uint) (argumentName (string-ascii 255))) + (map-get? PrincipalArgumentsByName { jobId: jobId, argumentName: argumentName }) +) + +(define-read-only (get-principal-argument-by-id (jobId uint) (argumentId uint)) + (map-get? PrincipalArgumentsById { jobId: jobId, argumentId: argumentId }) +) + +(define-read-only (get-principal-value-by-name (jobId uint) (argumentName (string-ascii 255))) + (get value (get-principal-argument-by-name jobId argumentName)) +) + +(define-read-only (get-principal-value-by-id (jobId uint) (argumentId uint)) + (get value (get-principal-argument-by-id jobId argumentId)) +) + +;; PRIVATE FUNCTIONS + +(define-read-only (is-approver (user principal)) + (default-to false (map-get? Approvers user)) +) + +(define-private (generate-argument-id (jobId uint) (argumentType (string-ascii 25))) + (let + ( + (argumentId (+ (default-to u0 (map-get? ArgumentLastIdsByType { jobId: jobId, argumentType: argumentType })) u1)) + ) + (map-set ArgumentLastIdsByType + { jobId: jobId, argumentType: argumentType } + argumentId + ) + ;; return + argumentId + ) +) + +(define-private (guard-add-argument (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) (err ERR_UNKNOWN_JOB))) + ) + (asserts! (not (get isActive job)) (err ERR_JOB_IS_ACTIVE)) + (asserts! (is-eq (get creator job) contract-caller) (err ERR_UNAUTHORIZED)) + (ok true) + ) +) + +;; CONTRACT MANAGEMENT + +;; initial value for active core contract +;; set to deployer address at startup to prevent +;; circular dependency of core on auth +(define-data-var activeCoreContract principal CONTRACT_OWNER) +(define-data-var initialized bool false) + +;; core contract states +(define-constant STATE_DEPLOYED u0) +(define-constant STATE_ACTIVE u1) +(define-constant STATE_INACTIVE u2) + +;; core contract map +(define-map CoreContracts + principal + { + state: uint, + startHeight: uint, + endHeight: uint + } +) + +;; getter for active core contract +(define-read-only (get-active-core-contract) + (begin + (asserts! (not (is-eq (var-get activeCoreContract) CONTRACT_OWNER)) (err ERR_NO_ACTIVE_CORE_CONTRACT)) + (ok (var-get activeCoreContract)) + ) +) + +;; getter for core contract map +(define-read-only (get-core-contract-info (targetContract principal)) + (let + ( + (coreContract (unwrap! (map-get? CoreContracts targetContract) (err ERR_CORE_CONTRACT_NOT_FOUND))) + ) + (ok coreContract) + ) +) + +;; one-time function to initialize contracts after all contracts are deployed +;; - check that deployer is calling this function +;; - check this contract is not activated already (one-time use) +;; - set initial map value for core contract v1 +;; - set cityWallet in core contract +;; - set intialized true +(define-public (initialize-contracts (coreContract )) + (let + ( + (coreContractAddress (contract-of coreContract)) + ) + (asserts! (is-eq contract-caller CONTRACT_OWNER) (err ERR_UNAUTHORIZED)) + (asserts! (not (var-get initialized)) (err ERR_UNAUTHORIZED)) + (map-set CoreContracts + coreContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (try! (contract-call? coreContract set-city-wallet (var-get cityWallet))) + (var-set initialized true) + (ok true) + ) +) + +(define-read-only (is-initialized) + (var-get initialized) +) + +;; function to activate core contract through registration +;; - check that target is in core contract map +;; - check that caller is core contract +;; - set active in core contract map +;; - set as activeCoreContract +(define-public (activate-core-contract (targetContract principal) (stacksHeight uint)) + (let + ( + (coreContract (unwrap! (map-get? CoreContracts targetContract) (err ERR_CORE_CONTRACT_NOT_FOUND))) + ) + (asserts! (is-eq contract-caller targetContract) (err ERR_UNAUTHORIZED)) + (map-set CoreContracts + targetContract + { + state: STATE_ACTIVE, + startHeight: stacksHeight, + endHeight: u0 + }) + (var-set activeCoreContract targetContract) + (ok true) + ) +) + +;; protected function to update core contract +(define-public (upgrade-core-contract (oldContract ) (newContract )) + (let + ( + (oldContractAddress (contract-of oldContract)) + (oldContractMap (unwrap! (map-get? CoreContracts oldContractAddress) (err ERR_CORE_CONTRACT_NOT_FOUND))) + (newContractAddress (contract-of newContract)) + ) + (asserts! (not (is-eq oldContractAddress newContractAddress)) (err ERR_UNAUTHORIZED)) + (asserts! (is-authorized-city) (err ERR_UNAUTHORIZED)) + (map-set CoreContracts + oldContractAddress + { + state: STATE_INACTIVE, + startHeight: (get startHeight oldContractMap), + endHeight: block-height + }) + (map-set CoreContracts + newContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (var-set activeCoreContract newContractAddress) + (try! (contract-call? oldContract shutdown-contract block-height)) + (try! (contract-call? newContract set-city-wallet (var-get cityWallet))) + (ok true) + ) +) + +(define-public (execute-upgrade-core-contract-job (jobId uint) (oldContract ) (newContract )) + (let + ( + (oldContractArg (unwrap! (get-principal-value-by-name jobId "oldContract") (err ERR_UNKNOWN_ARGUMENT))) + (newContractArg (unwrap! (get-principal-value-by-name jobId "newContract") (err ERR_UNKNOWN_ARGUMENT))) + (oldContractAddress (contract-of oldContract)) + (oldContractMap (unwrap! (map-get? CoreContracts oldContractAddress) (err ERR_CORE_CONTRACT_NOT_FOUND))) + (newContractAddress (contract-of newContract)) + ) + (asserts! (is-approver contract-caller) (err ERR_UNAUTHORIZED)) + (asserts! (and (is-eq oldContractArg oldContractAddress) (is-eq newContractArg newContractAddress)) (err ERR_UNAUTHORIZED)) + (asserts! (not (is-eq oldContractAddress newContractAddress)) (err ERR_UNAUTHORIZED)) + (map-set CoreContracts + oldContractAddress + { + state: STATE_INACTIVE, + startHeight: (get startHeight oldContractMap), + endHeight: block-height + }) + (map-set CoreContracts + newContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (var-set activeCoreContract newContractAddress) + (try! (contract-call? oldContract shutdown-contract block-height)) + (try! (contract-call? newContract set-city-wallet (var-get cityWallet))) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; CITY WALLET MANAGEMENT + +;; initial value for city wallet +(define-data-var cityWallet principal 'SM18VBF2QYAAHN57Q28E2HSM15F6078JZYZ2FQBCX) + +;; returns city wallet principal +(define-read-only (get-city-wallet) + (ok (var-get cityWallet)) +) + +;; protected function to update city wallet variable +(define-public (set-city-wallet (targetContract ) (newCityWallet principal)) + (let + ( + (coreContractAddress (contract-of targetContract)) + (coreContract (unwrap! (map-get? CoreContracts coreContractAddress) (err ERR_CORE_CONTRACT_NOT_FOUND))) + ) + (asserts! (is-authorized-city) (err ERR_UNAUTHORIZED)) + (asserts! (is-eq coreContractAddress (var-get activeCoreContract)) (err ERR_UNAUTHORIZED)) + (var-set cityWallet newCityWallet) + (try! (contract-call? targetContract set-city-wallet newCityWallet)) + (ok true) + ) +) + +(define-public (execute-set-city-wallet-job (jobId uint) (targetContract )) + (let + ( + (coreContractAddress (contract-of targetContract)) + (coreContract (unwrap! (map-get? CoreContracts coreContractAddress) (err ERR_CORE_CONTRACT_NOT_FOUND))) + (newCityWallet (unwrap! (get-principal-value-by-name jobId "newCityWallet") (err ERR_UNKNOWN_ARGUMENT))) + ) + (asserts! (is-approver contract-caller) (err ERR_UNAUTHORIZED)) + (asserts! (is-eq coreContractAddress (var-get activeCoreContract)) (err ERR_UNAUTHORIZED)) + (var-set cityWallet newCityWallet) + (try! (contract-call? targetContract set-city-wallet newCityWallet)) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; check if contract caller is city wallet +(define-private (is-authorized-city) + (is-eq contract-caller (var-get cityWallet)) +) + +;; TOKEN MANAGEMENT + +(define-public (set-token-uri (targetContract ) (newUri (optional (string-utf8 256)))) + (begin + (asserts! (is-authorized-city) (err ERR_UNAUTHORIZED)) + (as-contract (try! (contract-call? targetContract set-token-uri newUri))) + (ok true) + ) +) + +;; APPROVERS MANAGEMENT + +(define-public (execute-replace-approver-job (jobId uint)) + (let + ( + (oldApprover (unwrap! (get-principal-value-by-name jobId "oldApprover") (err ERR_UNKNOWN_ARGUMENT))) + (newApprover (unwrap! (get-principal-value-by-name jobId "newApprover") (err ERR_UNKNOWN_ARGUMENT))) + ) + (asserts! (is-approver contract-caller) (err ERR_UNAUTHORIZED)) + (map-set Approvers oldApprover false) + (map-set Approvers newApprover true) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; CONTRACT INITIALIZATION + +(map-insert Approvers 'SP372JVX6EWE2M0XPA84MWZYRRG2M6CAC4VVC12V1 true) +(map-insert Approvers 'SP2R0DQYR7XHD161SH2GK49QRP1YSV7HE9JSG7W6G true) +(map-insert Approvers 'SPN4Y5QPGQA8882ZXW90ADC2DHYXMSTN8VAR8C3X true) +(map-insert Approvers 'SP3YYGCGX1B62CYAH4QX7PQE63YXG7RDTXD8BQHJQ true) +(map-insert Approvers 'SP7DGES13508FHRWS1FB0J3SZA326FP6QRMB6JDE true) diff --git a/.cache/requirements/SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-auth.json b/.cache/requirements/SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-auth.json new file mode 100644 index 00000000..11162a13 --- /dev/null +++ b/.cache/requirements/SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-auth.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch20", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token.clar b/.cache/requirements/SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token.clar new file mode 100644 index 00000000..0a04a100 --- /dev/null +++ b/.cache/requirements/SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token.clar @@ -0,0 +1,177 @@ +;; NEWYORKCITYCOIN TOKEN CONTRACT +;; CityCoins Protocol Version 1.0.1 + +;; CONTRACT OWNER + +(define-constant CONTRACT_OWNER tx-sender) + +;; TRAIT DEFINITIONS + +(impl-trait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-token-trait.citycoin-token) +(use-trait coreTrait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.citycoin-core) + +;; ERROR CODES + +(define-constant ERR_UNAUTHORIZED u2000) +(define-constant ERR_TOKEN_NOT_ACTIVATED u2001) +(define-constant ERR_TOKEN_ALREADY_ACTIVATED u2002) + +;; SIP-010 DEFINITION + +(impl-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait) + +(define-fungible-token newyorkcitycoin) + +;; SIP-010 FUNCTIONS + +(define-public (transfer (amount uint) (from principal) (to principal) (memo (optional (buff 34)))) + (begin + (asserts! (is-eq from tx-sender) (err ERR_UNAUTHORIZED)) + (if (is-some memo) + (print memo) + none + ) + (ft-transfer? newyorkcitycoin amount from to) + ) +) + +(define-read-only (get-name) + (ok "newyorkcitycoin") +) + +(define-read-only (get-symbol) + (ok "NYC") +) + +(define-read-only (get-decimals) + (ok u0) +) + +(define-read-only (get-balance (user principal)) + (ok (ft-get-balance newyorkcitycoin user)) +) + +(define-read-only (get-total-supply) + (ok (ft-get-supply newyorkcitycoin)) +) + +(define-read-only (get-token-uri) + (ok (var-get tokenUri)) +) + +;; TOKEN CONFIGURATION + +;; how many blocks until the next halving occurs +(define-constant TOKEN_HALVING_BLOCKS u210000) + +;; store block height at each halving, set by register-user in core contract +(define-data-var coinbaseThreshold1 uint u0) +(define-data-var coinbaseThreshold2 uint u0) +(define-data-var coinbaseThreshold3 uint u0) +(define-data-var coinbaseThreshold4 uint u0) +(define-data-var coinbaseThreshold5 uint u0) + +;; once activated, thresholds cannot be updated again +(define-data-var tokenActivated bool false) + +;; core contract states +(define-constant STATE_DEPLOYED u0) +(define-constant STATE_ACTIVE u1) +(define-constant STATE_INACTIVE u2) + +;; one-time function to activate the token +(define-public (activate-token (coreContract principal) (stacksHeight uint)) + (let + ( + (coreContractMap (try! (contract-call? 'SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-auth get-core-contract-info coreContract))) + ) + (asserts! (is-eq (get state coreContractMap) STATE_ACTIVE) (err ERR_UNAUTHORIZED)) + (asserts! (not (var-get tokenActivated)) (err ERR_TOKEN_ALREADY_ACTIVATED)) + (var-set tokenActivated true) + (var-set coinbaseThreshold1 (+ stacksHeight TOKEN_HALVING_BLOCKS)) + (var-set coinbaseThreshold2 (+ stacksHeight (* u2 TOKEN_HALVING_BLOCKS))) + (var-set coinbaseThreshold3 (+ stacksHeight (* u3 TOKEN_HALVING_BLOCKS))) + (var-set coinbaseThreshold4 (+ stacksHeight (* u4 TOKEN_HALVING_BLOCKS))) + (var-set coinbaseThreshold5 (+ stacksHeight (* u5 TOKEN_HALVING_BLOCKS))) + (ok true) + ) +) + +;; return coinbase thresholds if token activated +(define-read-only (get-coinbase-thresholds) + (let + ( + (activated (var-get tokenActivated)) + ) + (asserts! activated (err ERR_TOKEN_NOT_ACTIVATED)) + (ok { + coinbaseThreshold1: (var-get coinbaseThreshold1), + coinbaseThreshold2: (var-get coinbaseThreshold2), + coinbaseThreshold3: (var-get coinbaseThreshold3), + coinbaseThreshold4: (var-get coinbaseThreshold4), + coinbaseThreshold5: (var-get coinbaseThreshold5) + }) + ) +) + +;; UTILITIES + +(define-data-var tokenUri (optional (string-utf8 256)) (some u"https://cdn.citycoins.co/metadata/newyorkcitycoin.json")) + +;; set token URI to new value, only accessible by Auth +(define-public (set-token-uri (newUri (optional (string-utf8 256)))) + (begin + (asserts! (is-authorized-auth) (err ERR_UNAUTHORIZED)) + (ok (var-set tokenUri newUri)) + ) +) + +;; mint new tokens, only accessible by a Core contract +(define-public (mint (amount uint) (recipient principal)) + (let + ( + (coreContract (try! (contract-call? 'SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-auth get-core-contract-info contract-caller))) + ) + (ft-mint? newyorkcitycoin amount recipient) + ) +) + +(define-public (burn (amount uint) (owner principal)) + (begin + (asserts! (is-eq tx-sender owner) (err ERR_UNAUTHORIZED)) + (ft-burn? newyorkcitycoin amount owner) + ) +) + +;; checks if caller is Auth contract +(define-private (is-authorized-auth) + (is-eq contract-caller 'SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-auth) +) + +;; SEND-MANY + +(define-public (send-many (recipients (list 200 { to: principal, amount: uint, memo: (optional (buff 34)) }))) + (fold check-err + (map send-token recipients) + (ok true) + ) +) + +(define-private (check-err (result (response bool uint)) (prior (response bool uint))) + (match prior ok-value result + err-value (err err-value) + ) +) + +(define-private (send-token (recipient { to: principal, amount: uint, memo: (optional (buff 34)) })) + (send-token-with-memo (get amount recipient) (get to recipient) (get memo recipient)) +) + +(define-private (send-token-with-memo (amount uint) (to principal) (memo (optional (buff 34)))) + (let + ( + (transferOk (try! (transfer amount tx-sender to memo))) + ) + (ok transferOk) + ) +) diff --git a/.cache/requirements/SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token.json b/.cache/requirements/SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token.json new file mode 100644 index 00000000..11162a13 --- /dev/null +++ b/.cache/requirements/SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch20", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.clar b/.cache/requirements/SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.clar new file mode 100644 index 00000000..cc558fdb --- /dev/null +++ b/.cache/requirements/SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.clar @@ -0,0 +1,15 @@ +(define-trait nft-trait + ( + ;; Last token ID, limited to uint range + (get-last-token-id () (response uint uint)) + + ;; URI for metadata associated with the token + (get-token-uri (uint) (response (optional (string-ascii 256)) uint)) + + ;; Owner of a given token identifier + (get-owner (uint) (response (optional principal) uint)) + + ;; Transfer from the sender to a new principal + (transfer (uint principal principal) (response bool uint)) + ) +) diff --git a/.cache/requirements/SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.json b/.cache/requirements/SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.json new file mode 100644 index 00000000..11162a13 --- /dev/null +++ b/.cache/requirements/SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch20", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.clar b/.cache/requirements/SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.clar new file mode 100644 index 00000000..69255cf9 --- /dev/null +++ b/.cache/requirements/SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.clar @@ -0,0 +1,24 @@ +(define-trait sip-010-trait + ( + ;; Transfer from the caller to a new principal + (transfer (uint principal principal (optional (buff 34))) (response bool uint)) + + ;; the human readable name of the token + (get-name () (response (string-ascii 32) uint)) + + ;; the ticker symbol, or empty if none + (get-symbol () (response (string-ascii 32) uint)) + + ;; the number of decimals used, e.g. 6 would mean 1_000_000 represents 1 token + (get-decimals () (response uint uint)) + + ;; the balance of the passed principal + (get-balance (principal) (response uint uint)) + + ;; the current total supply (which does not need to be a constant) + (get-total-supply () (response uint uint)) + + ;; an optional URI that represents metadata of this token + (get-token-uri () (response (optional (string-utf8 256)) uint)) + ) +) diff --git a/.cache/requirements/SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.json b/.cache/requirements/SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.json new file mode 100644 index 00000000..11162a13 --- /dev/null +++ b/.cache/requirements/SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch20", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.clar b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.clar new file mode 100644 index 00000000..b2945329 --- /dev/null +++ b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.clar @@ -0,0 +1,35 @@ +;; CITYCOIN CORE TRAIT + +(define-trait citycoin-core + ( + + (register-user ((optional (string-utf8 50))) + (response bool uint) + ) + + (mine-tokens (uint (optional (buff 34))) + (response bool uint) + ) + + (claim-mining-reward (uint) + (response bool uint) + ) + + (stack-tokens (uint uint) + (response bool uint) + ) + + (claim-stacking-reward (uint) + (response bool uint) + ) + + (set-city-wallet (principal) + (response bool uint) + ) + + (shutdown-contract (uint) + (response bool uint) + ) + + ) +) diff --git a/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.json b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.json new file mode 100644 index 00000000..11162a13 --- /dev/null +++ b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch20", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-token-trait.clar b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-token-trait.clar new file mode 100644 index 00000000..107b4e41 --- /dev/null +++ b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-token-trait.clar @@ -0,0 +1,27 @@ +;; CITYCOIN TOKEN TRAIT + +(define-trait citycoin-token + ( + + (activate-token (principal uint) + (response bool uint) + ) + + (set-token-uri ((optional (string-utf8 256))) + (response bool uint) + ) + + (mint (uint principal) + (response bool uint) + ) + + (burn (uint principal) + (response bool uint) + ) + + (send-many ((list 200 { to: principal, amount: uint, memo: (optional (buff 34)) })) + (response bool uint) + ) + + ) +) diff --git a/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-token-trait.json b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-token-trait.json new file mode 100644 index 00000000..11162a13 --- /dev/null +++ b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-token-trait.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch20", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-auth.clar b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-auth.clar new file mode 100644 index 00000000..9f26454a --- /dev/null +++ b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-auth.clar @@ -0,0 +1,563 @@ +;; MIAMICOIN AUTH CONTRACT + +(define-constant CONTRACT_OWNER tx-sender) + +;; TRAIT DEFINITIONS + +(use-trait coreTrait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.citycoin-core) +(use-trait tokenTrait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-token-trait.citycoin-token) + +;; ERRORS + +(define-constant ERR_UNKNOWN_JOB u6000) +(define-constant ERR_UNAUTHORIZED u6001) +(define-constant ERR_JOB_IS_ACTIVE u6002) +(define-constant ERR_JOB_IS_NOT_ACTIVE u6003) +(define-constant ERR_ALREADY_VOTED_THIS_WAY u6004) +(define-constant ERR_JOB_IS_EXECUTED u6005) +(define-constant ERR_JOB_IS_NOT_APPROVED u6006) +(define-constant ERR_ARGUMENT_ALREADY_EXISTS u6007) +(define-constant ERR_NO_ACTIVE_CORE_CONTRACT u6008) +(define-constant ERR_CORE_CONTRACT_NOT_FOUND u6009) +(define-constant ERR_UNKNOWN_ARGUMENT u6010) + +;; JOB MANAGEMENT + +(define-constant REQUIRED_APPROVALS u3) + +(define-data-var lastJobId uint u0) + +(define-map Jobs + uint + { + creator: principal, + name: (string-ascii 255), + target: principal, + approvals: uint, + disapprovals: uint, + isActive: bool, + isExecuted: bool + } +) + +(define-map JobApprovers + { jobId: uint, approver: principal } + bool +) + +(define-map Approvers + principal + bool +) + +(define-map ArgumentLastIdsByType + { jobId: uint, argumentType: (string-ascii 25) } + uint +) + +(define-map UIntArgumentsByName + { jobId: uint, argumentName: (string-ascii 255) } + { argumentId: uint, value: uint} +) + +(define-map UIntArgumentsById + { jobId: uint, argumentId: uint } + { argumentName: (string-ascii 255), value: uint } +) + +(define-map PrincipalArgumentsByName + { jobId: uint, argumentName: (string-ascii 255) } + { argumentId: uint, value: principal } +) + +(define-map PrincipalArgumentsById + { jobId: uint, argumentId: uint } + { argumentName: (string-ascii 255), value: principal } +) + +;; FUNCTIONS + +(define-read-only (get-last-job-id) + (var-get lastJobId) +) + +(define-public (create-job (name (string-ascii 255)) (target principal)) + (let + ( + (newJobId (+ (var-get lastJobId) u1)) + ) + (asserts! (is-approver tx-sender) (err ERR_UNAUTHORIZED)) + (map-set Jobs + newJobId + { + creator: tx-sender, + name: name, + target: target, + approvals: u0, + disapprovals: u0, + isActive: false, + isExecuted: false + } + ) + (var-set lastJobId newJobId) + (ok newJobId) + ) +) + +(define-read-only (get-job (jobId uint)) + (map-get? Jobs jobId) +) + +(define-public (activate-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) (err ERR_UNKNOWN_JOB))) + ) + (asserts! (is-eq (get creator job) tx-sender) (err ERR_UNAUTHORIZED)) + (asserts! (not (get isActive job)) (err ERR_JOB_IS_ACTIVE)) + (map-set Jobs + jobId + (merge job { isActive: true }) + ) + (ok true) + ) +) + +(define-public (approve-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) (err ERR_UNKNOWN_JOB))) + (previousVote (map-get? JobApprovers { jobId: jobId, approver: tx-sender })) + ) + (asserts! (get isActive job) (err ERR_JOB_IS_NOT_ACTIVE)) + (asserts! (is-approver tx-sender) (err ERR_UNAUTHORIZED)) + ;; save vote + (map-set JobApprovers + { jobId: jobId, approver: tx-sender } + true + ) + (match previousVote approved + (begin + (asserts! (not approved) (err ERR_ALREADY_VOTED_THIS_WAY)) + (map-set Jobs jobId + (merge job + { + approvals: (+ (get approvals job) u1), + disapprovals: (- (get disapprovals job) u1) + } + ) + ) + ) + ;; no previous vote + (map-set Jobs + jobId + (merge job { approvals: (+ (get approvals job) u1) } ) + ) + ) + (ok true) + ) +) + +(define-public (disapprove-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) (err ERR_UNKNOWN_JOB))) + (previousVote (map-get? JobApprovers { jobId: jobId, approver: tx-sender })) + ) + (asserts! (get isActive job) (err ERR_JOB_IS_NOT_ACTIVE)) + (asserts! (is-approver tx-sender) (err ERR_UNAUTHORIZED)) + ;; save vote + (map-set JobApprovers + { jobId: jobId, approver: tx-sender } + false + ) + (match previousVote approved + (begin + (asserts! approved (err ERR_ALREADY_VOTED_THIS_WAY)) + (map-set Jobs jobId + (merge job + { + approvals: (- (get approvals job) u1), + disapprovals: (+ (get disapprovals job) u1) + } + ) + ) + ) + ;; no previous vote + (map-set Jobs + jobId + (merge job { disapprovals: (+ (get disapprovals job) u1) } ) + ) + ) + (ok true) + ) +) + +(define-read-only (is-job-approved (jobId uint)) + (match (get-job jobId) job + (>= (get approvals job) REQUIRED_APPROVALS) + false + ) +) + +(define-public (mark-job-as-executed (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) (err ERR_UNKNOWN_JOB))) + ) + (asserts! (get isActive job) (err ERR_JOB_IS_NOT_ACTIVE)) + (asserts! (>= (get approvals job) REQUIRED_APPROVALS) (err ERR_JOB_IS_NOT_APPROVED)) + (asserts! (is-eq (get target job) contract-caller) (err ERR_UNAUTHORIZED)) + (asserts! (not (get isExecuted job)) (err ERR_JOB_IS_EXECUTED)) + (map-set Jobs + jobId + (merge job { isExecuted: true }) + ) + (ok true) + ) +) + +(define-public (add-uint-argument (jobId uint) (argumentName (string-ascii 255)) (value uint)) + (let + ( + (argumentId (generate-argument-id jobId "uint")) + ) + (try! (guard-add-argument jobId)) + (asserts! + (and + (map-insert UIntArgumentsById + { jobId: jobId, argumentId: argumentId } + { argumentName: argumentName, value: value } + ) + (map-insert UIntArgumentsByName + { jobId: jobId, argumentName: argumentName } + { argumentId: argumentId, value: value} + ) + ) + (err ERR_ARGUMENT_ALREADY_EXISTS) + ) + (ok true) + ) +) + +(define-read-only (get-uint-argument-by-name (jobId uint) (argumentName (string-ascii 255))) + (map-get? UIntArgumentsByName { jobId: jobId, argumentName: argumentName }) +) + +(define-read-only (get-uint-argument-by-id (jobId uint) (argumentId uint)) + (map-get? UIntArgumentsById { jobId: jobId, argumentId: argumentId }) +) + +(define-read-only (get-uint-value-by-name (jobId uint) (argumentName (string-ascii 255))) + (get value (get-uint-argument-by-name jobId argumentName)) +) + +(define-read-only (get-uint-value-by-id (jobId uint) (argumentId uint)) + (get value (get-uint-argument-by-id jobId argumentId)) +) + +(define-public (add-principal-argument (jobId uint) (argumentName (string-ascii 255)) (value principal)) + (let + ( + (argumentId (generate-argument-id jobId "principal")) + ) + (try! (guard-add-argument jobId)) + (asserts! + (and + (map-insert PrincipalArgumentsById + { jobId: jobId, argumentId: argumentId } + { argumentName: argumentName, value: value } + ) + (map-insert PrincipalArgumentsByName + { jobId: jobId, argumentName: argumentName } + { argumentId: argumentId, value: value} + ) + ) + (err ERR_ARGUMENT_ALREADY_EXISTS) + ) + (ok true) + ) +) + +(define-read-only (get-principal-argument-by-name (jobId uint) (argumentName (string-ascii 255))) + (map-get? PrincipalArgumentsByName { jobId: jobId, argumentName: argumentName }) +) + +(define-read-only (get-principal-argument-by-id (jobId uint) (argumentId uint)) + (map-get? PrincipalArgumentsById { jobId: jobId, argumentId: argumentId }) +) + +(define-read-only (get-principal-value-by-name (jobId uint) (argumentName (string-ascii 255))) + (get value (get-principal-argument-by-name jobId argumentName)) +) + +(define-read-only (get-principal-value-by-id (jobId uint) (argumentId uint)) + (get value (get-principal-argument-by-id jobId argumentId)) +) + +;; PRIVATE FUNCTIONS + +(define-read-only (is-approver (user principal)) + (default-to false (map-get? Approvers user)) +) + +(define-private (generate-argument-id (jobId uint) (argumentType (string-ascii 25))) + (let + ( + (argumentId (+ (default-to u0 (map-get? ArgumentLastIdsByType { jobId: jobId, argumentType: argumentType })) u1)) + ) + (map-set ArgumentLastIdsByType + { jobId: jobId, argumentType: argumentType } + argumentId + ) + ;; return + argumentId + ) +) + +(define-private (guard-add-argument (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) (err ERR_UNKNOWN_JOB))) + ) + (asserts! (not (get isActive job)) (err ERR_JOB_IS_ACTIVE)) + (asserts! (is-eq (get creator job) contract-caller) (err ERR_UNAUTHORIZED)) + (ok true) + ) +) + +;; CONTRACT MANAGEMENT + +;; initial value for active core contract +;; set to deployer address at startup to prevent +;; circular dependency of core on auth +(define-data-var activeCoreContract principal CONTRACT_OWNER) +(define-data-var initialized bool false) + +;; core contract states +(define-constant STATE_DEPLOYED u0) +(define-constant STATE_ACTIVE u1) +(define-constant STATE_INACTIVE u2) + +;; core contract map +(define-map CoreContracts + principal + { + state: uint, + startHeight: uint, + endHeight: uint + } +) + +;; getter for active core contract +(define-read-only (get-active-core-contract) + (begin + (asserts! (not (is-eq (var-get activeCoreContract) CONTRACT_OWNER)) (err ERR_NO_ACTIVE_CORE_CONTRACT)) + (ok (var-get activeCoreContract)) + ) +) + +;; getter for core contract map +(define-read-only (get-core-contract-info (targetContract principal)) + (let + ( + (coreContract (unwrap! (map-get? CoreContracts targetContract) (err ERR_CORE_CONTRACT_NOT_FOUND))) + ) + (ok coreContract) + ) +) + +;; one-time function to initialize contracts after all contracts are deployed +;; - check that deployer is calling this function +;; - check this contract is not activated already (one-time use) +;; - set initial map value for core contract v1 +;; - set cityWallet in core contract +;; - set intialized true +(define-public (initialize-contracts (coreContract )) + (let + ( + (coreContractAddress (contract-of coreContract)) + ) + (asserts! (is-eq contract-caller CONTRACT_OWNER) (err ERR_UNAUTHORIZED)) + (asserts! (not (var-get initialized)) (err ERR_UNAUTHORIZED)) + (map-set CoreContracts + coreContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (try! (contract-call? coreContract set-city-wallet (var-get cityWallet))) + (var-set initialized true) + (ok true) + ) +) + +(define-read-only (is-initialized) + (var-get initialized) +) + +;; function to activate core contract through registration +;; - check that target is in core contract map +;; - check that caller is core contract +;; - set active in core contract map +;; - set as activeCoreContract +(define-public (activate-core-contract (targetContract principal) (stacksHeight uint)) + (let + ( + (coreContract (unwrap! (map-get? CoreContracts targetContract) (err ERR_CORE_CONTRACT_NOT_FOUND))) + ) + (asserts! (is-eq contract-caller targetContract) (err ERR_UNAUTHORIZED)) + (map-set CoreContracts + targetContract + { + state: STATE_ACTIVE, + startHeight: stacksHeight, + endHeight: u0 + }) + (var-set activeCoreContract targetContract) + (ok true) + ) +) + +;; protected function to update core contract +(define-public (upgrade-core-contract (oldContract ) (newContract )) + (let + ( + (oldContractAddress (contract-of oldContract)) + (oldContractMap (unwrap! (map-get? CoreContracts oldContractAddress) (err ERR_CORE_CONTRACT_NOT_FOUND))) + (newContractAddress (contract-of newContract)) + ) + (asserts! (not (is-eq oldContractAddress newContractAddress)) (err ERR_UNAUTHORIZED)) + (asserts! (is-authorized-city) (err ERR_UNAUTHORIZED)) + (map-set CoreContracts + oldContractAddress + { + state: STATE_INACTIVE, + startHeight: (get startHeight oldContractMap), + endHeight: block-height + }) + (map-set CoreContracts + newContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (var-set activeCoreContract newContractAddress) + (try! (contract-call? oldContract shutdown-contract block-height)) + (try! (contract-call? newContract set-city-wallet (var-get cityWallet))) + (ok true) + ) +) + +(define-public (execute-upgrade-core-contract-job (jobId uint) (oldContract ) (newContract )) + (let + ( + (oldContractArg (unwrap! (get-principal-value-by-name jobId "oldContract") (err ERR_UNKNOWN_ARGUMENT))) + (newContractArg (unwrap! (get-principal-value-by-name jobId "newContract") (err ERR_UNKNOWN_ARGUMENT))) + (oldContractAddress (contract-of oldContract)) + (oldContractMap (unwrap! (map-get? CoreContracts oldContractAddress) (err ERR_CORE_CONTRACT_NOT_FOUND))) + (newContractAddress (contract-of newContract)) + ) + (asserts! (is-approver contract-caller) (err ERR_UNAUTHORIZED)) + (asserts! (and (is-eq oldContractArg oldContractAddress) (is-eq newContractArg newContractAddress)) (err ERR_UNAUTHORIZED)) + (asserts! (not (is-eq oldContractAddress newContractAddress)) (err ERR_UNAUTHORIZED)) + (map-set CoreContracts + oldContractAddress + { + state: STATE_INACTIVE, + startHeight: (get startHeight oldContractMap), + endHeight: block-height + }) + (map-set CoreContracts + newContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (var-set activeCoreContract newContractAddress) + (try! (contract-call? oldContract shutdown-contract block-height)) + (try! (contract-call? newContract set-city-wallet (var-get cityWallet))) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; CITY WALLET MANAGEMENT + +;; initial value for city wallet +(define-data-var cityWallet principal 'SM2MARAVW6BEJCD13YV2RHGYHQWT7TDDNMNRB1MVT) + +;; returns city wallet principal +(define-read-only (get-city-wallet) + (ok (var-get cityWallet)) +) + +;; protected function to update city wallet variable +(define-public (set-city-wallet (targetContract ) (newCityWallet principal)) + (let + ( + (coreContractAddress (contract-of targetContract)) + (coreContract (unwrap! (map-get? CoreContracts coreContractAddress) (err ERR_CORE_CONTRACT_NOT_FOUND))) + ) + (asserts! (is-authorized-city) (err ERR_UNAUTHORIZED)) + (asserts! (is-eq coreContractAddress (var-get activeCoreContract)) (err ERR_UNAUTHORIZED)) + (var-set cityWallet newCityWallet) + (try! (contract-call? targetContract set-city-wallet newCityWallet)) + (ok true) + ) +) + +(define-public (execute-set-city-wallet-job (jobId uint) (targetContract )) + (let + ( + (coreContractAddress (contract-of targetContract)) + (coreContract (unwrap! (map-get? CoreContracts coreContractAddress) (err ERR_CORE_CONTRACT_NOT_FOUND))) + (newCityWallet (unwrap! (get-principal-value-by-name jobId "newCityWallet") (err ERR_UNKNOWN_ARGUMENT))) + ) + (asserts! (is-approver contract-caller) (err ERR_UNAUTHORIZED)) + (asserts! (is-eq coreContractAddress (var-get activeCoreContract)) (err ERR_UNAUTHORIZED)) + (var-set cityWallet newCityWallet) + (try! (contract-call? targetContract set-city-wallet newCityWallet)) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; check if contract caller is city wallet +(define-private (is-authorized-city) + (is-eq contract-caller (var-get cityWallet)) +) + +;; TOKEN MANAGEMENT + +(define-public (set-token-uri (targetContract ) (newUri (optional (string-utf8 256)))) + (begin + (asserts! (is-authorized-city) (err ERR_UNAUTHORIZED)) + (as-contract (try! (contract-call? targetContract set-token-uri newUri))) + (ok true) + ) +) + +;; APPROVERS MANAGEMENT + +(define-public (execute-replace-approver-job (jobId uint)) + (let + ( + (oldApprover (unwrap! (get-principal-value-by-name jobId "oldApprover") (err ERR_UNKNOWN_ARGUMENT))) + (newApprover (unwrap! (get-principal-value-by-name jobId "newApprover") (err ERR_UNKNOWN_ARGUMENT))) + ) + (asserts! (is-approver contract-caller) (err ERR_UNAUTHORIZED)) + (map-set Approvers oldApprover false) + (map-set Approvers newApprover true) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; CONTRACT INITIALIZATION + +(map-insert Approvers 'SP372JVX6EWE2M0XPA84MWZYRRG2M6CAC4VVC12V1 true) +(map-insert Approvers 'SP2R0DQYR7XHD161SH2GK49QRP1YSV7HE9JSG7W6G true) +(map-insert Approvers 'SPN4Y5QPGQA8882ZXW90ADC2DHYXMSTN8VAR8C3X true) +(map-insert Approvers 'SP3YYGCGX1B62CYAH4QX7PQE63YXG7RDTXD8BQHJQ true) +(map-insert Approvers 'SP7DGES13508FHRWS1FB0J3SZA326FP6QRMB6JDE true) diff --git a/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-auth.json b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-auth.json new file mode 100644 index 00000000..11162a13 --- /dev/null +++ b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-auth.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch20", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-token.clar b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-token.clar new file mode 100644 index 00000000..5631b64f --- /dev/null +++ b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-token.clar @@ -0,0 +1,179 @@ +;; MIAMICOIN TOKEN CONTRACT + +;; CONTRACT OWNER + +(define-constant CONTRACT_OWNER tx-sender) + +;; TRAIT DEFINITIONS + +(impl-trait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-token-trait.citycoin-token) +(use-trait coreTrait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.citycoin-core) + +;; ERROR CODES + +(define-constant ERR_UNAUTHORIZED u2000) +(define-constant ERR_TOKEN_NOT_ACTIVATED u2001) +(define-constant ERR_TOKEN_ALREADY_ACTIVATED u2002) + +;; SIP-010 DEFINITION + +(impl-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait) + +(define-fungible-token miamicoin) + +;; SIP-010 FUNCTIONS + +(define-public (transfer (amount uint) (from principal) (to principal) (memo (optional (buff 34)))) + (begin + (asserts! (is-eq from tx-sender) (err ERR_UNAUTHORIZED)) + (if (is-some memo) + (print memo) + none + ) + (ft-transfer? miamicoin amount from to) + ) +) + +(define-read-only (get-name) + (ok "miamicoin") +) + +(define-read-only (get-symbol) + (ok "MIA") +) + +(define-read-only (get-decimals) + (ok u0) +) + +(define-read-only (get-balance (user principal)) + (ok (ft-get-balance miamicoin user)) +) + +(define-read-only (get-total-supply) + (ok (ft-get-supply miamicoin)) +) + +(define-read-only (get-token-uri) + (ok (var-get tokenUri)) +) + +;; TOKEN CONFIGURATION + +;; how many blocks until the next halving occurs +(define-constant TOKEN_HALVING_BLOCKS u210000) + +;; store block height at each halving, set by register-user in core contract +(define-data-var coinbaseThreshold1 uint u0) +(define-data-var coinbaseThreshold2 uint u0) +(define-data-var coinbaseThreshold3 uint u0) +(define-data-var coinbaseThreshold4 uint u0) +(define-data-var coinbaseThreshold5 uint u0) + +;; once activated, thresholds cannot be updated again +(define-data-var tokenActivated bool false) + +;; core contract states +(define-constant STATE_DEPLOYED u0) +(define-constant STATE_ACTIVE u1) +(define-constant STATE_INACTIVE u2) + +;; one-time function to activate the token +(define-public (activate-token (coreContract principal) (stacksHeight uint)) + (let + ( + (coreContractMap (try! (contract-call? 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-auth get-core-contract-info coreContract))) + ) + (asserts! (is-eq (get state coreContractMap) STATE_ACTIVE) (err ERR_UNAUTHORIZED)) + (asserts! (not (var-get tokenActivated)) (err ERR_TOKEN_ALREADY_ACTIVATED)) + (var-set tokenActivated true) + (var-set coinbaseThreshold1 (+ stacksHeight TOKEN_HALVING_BLOCKS)) + (var-set coinbaseThreshold2 (+ stacksHeight (* u2 TOKEN_HALVING_BLOCKS))) + (var-set coinbaseThreshold3 (+ stacksHeight (* u3 TOKEN_HALVING_BLOCKS))) + (var-set coinbaseThreshold4 (+ stacksHeight (* u4 TOKEN_HALVING_BLOCKS))) + (var-set coinbaseThreshold5 (+ stacksHeight (* u5 TOKEN_HALVING_BLOCKS))) + (ok true) + ) +) + +;; return coinbase thresholds if token activated +(define-read-only (get-coinbase-thresholds) + (let + ( + (activated (var-get tokenActivated)) + ) + (asserts! activated (err ERR_TOKEN_NOT_ACTIVATED)) + (ok { + coinbaseThreshold1: (var-get coinbaseThreshold1), + coinbaseThreshold2: (var-get coinbaseThreshold2), + coinbaseThreshold3: (var-get coinbaseThreshold3), + coinbaseThreshold4: (var-get coinbaseThreshold4), + coinbaseThreshold5: (var-get coinbaseThreshold5) + }) + ) +) + +;; UTILITIES + +(define-data-var tokenUri (optional (string-utf8 256)) (some u"https://cdn.citycoins.co/metadata/miamicoin.json")) + +;; set token URI to new value, only accessible by Auth +(define-public (set-token-uri (newUri (optional (string-utf8 256)))) + (begin + (asserts! (is-authorized-auth) (err ERR_UNAUTHORIZED)) + (ok (var-set tokenUri newUri)) + ) +) + +;; mint new tokens, only accessible by a Core contract +(define-public (mint (amount uint) (recipient principal)) + (let + ( + (coreContract (try! (contract-call? 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-auth get-core-contract-info contract-caller))) + ) + (ft-mint? miamicoin amount recipient) + ) +) + +;; burn tokens, only accessible by a Core contract +(define-public (burn (amount uint) (recipient principal)) + (let + ( + (coreContract (try! (contract-call? 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-auth get-core-contract-info contract-caller))) + ) + (ft-burn? miamicoin amount recipient) + ) +) + +;; checks if caller is Auth contract +(define-private (is-authorized-auth) + (is-eq contract-caller 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-auth) +) + +;; SEND-MANY + +(define-public (send-many (recipients (list 200 { to: principal, amount: uint, memo: (optional (buff 34)) }))) + (fold check-err + (map send-token recipients) + (ok true) + ) +) + +(define-private (check-err (result (response bool uint)) (prior (response bool uint))) + (match prior ok-value result + err-value (err err-value) + ) +) + +(define-private (send-token (recipient { to: principal, amount: uint, memo: (optional (buff 34)) })) + (send-token-with-memo (get amount recipient) (get to recipient) (get memo recipient)) +) + +(define-private (send-token-with-memo (amount uint) (to principal) (memo (optional (buff 34)))) + (let + ( + (transferOk (try! (transfer amount tx-sender to memo))) + ) + (ok transferOk) + ) +) diff --git a/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-token.json b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-token.json new file mode 100644 index 00000000..11162a13 --- /dev/null +++ b/.cache/requirements/SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-token.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch20", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-core-v2-trait.clar b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-core-v2-trait.clar new file mode 100644 index 00000000..ebb65396 --- /dev/null +++ b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-core-v2-trait.clar @@ -0,0 +1,47 @@ +;; CITYCOIN CORE TRAIT V2 + +(define-trait citycoin-core-v2 + ( + + (register-user ((optional (string-utf8 50))) + (response bool uint) + ) + + (mine-tokens (uint (optional (buff 34))) + (response bool uint) + ) + + (mine-many ((list 200 uint)) + (response bool uint) + ) + + (claim-mining-reward (uint) + (response bool uint) + ) + + (stack-tokens (uint uint) + (response bool uint) + ) + + (claim-stacking-reward (uint) + (response bool uint) + ) + + (set-city-wallet (principal) + (response bool uint) + ) + + (update-coinbase-amounts () + (response bool uint) + ) + + (update-coinbase-thresholds () + (response bool uint) + ) + + (shutdown-contract (uint) + (response bool uint) + ) + + ) +) diff --git a/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-core-v2-trait.json b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-core-v2-trait.json new file mode 100644 index 00000000..89c8a70a --- /dev/null +++ b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-core-v2-trait.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-token-v2-trait.clar b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-token-v2-trait.clar new file mode 100644 index 00000000..794ebda8 --- /dev/null +++ b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-token-v2-trait.clar @@ -0,0 +1,35 @@ +;; CITYCOIN TOKEN TRAIT + +(define-trait citycoin-token-v2 + ( + + (activate-token (principal uint) + (response bool uint) + ) + + (set-token-uri ((optional (string-utf8 256))) + (response bool uint) + ) + + (mint (uint principal) + (response bool uint) + ) + + (burn (uint principal) + (response bool uint) + ) + + (send-many ((list 200 { to: principal, amount: uint, memo: (optional (buff 34)) })) + (response bool uint) + ) + + (update-coinbase-thresholds (uint uint uint uint uint) + (response bool uint) + ) + + (update-coinbase-amounts (uint uint uint uint uint uint uint) + (response bool uint) + ) + + ) +) diff --git a/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-token-v2-trait.json b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-token-v2-trait.json new file mode 100644 index 00000000..89c8a70a --- /dev/null +++ b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-token-v2-trait.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-vrf-v2.clar b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-vrf-v2.clar new file mode 100644 index 00000000..1278c676 --- /dev/null +++ b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-vrf-v2.clar @@ -0,0 +1,69 @@ +;; CITYCOIN VRF CONTRACT V2 +;; CityCoins Protocol Version 2.0.0 + +;; ERROR CODES + +(define-constant ERR_FAIL (err u3000)) + +;; CONFIGURATION + +(define-map RandomUintAtBlock uint uint) + +;; PUBLIC FUNCTIONS + +;; returns the saved random integer +;; or, calculates and saves it to the map +;; #[allow(unchecked_data)] +(define-public (get-save-rnd (block uint)) + (match (map-get? RandomUintAtBlock block) + rnd (ok rnd) + (match (read-rnd block) + rnd (begin (map-set RandomUintAtBlock block rnd) (ok rnd)) + err-val (err err-val) + ) + ) +) + +;; returns the saved random integer +;; or, calculates and returns the value +(define-read-only (get-rnd (block uint)) + (match (map-get? RandomUintAtBlock block) + rnd (ok rnd) + (read-rnd block) + ) +) + +;; PRIVATE FUNCTIONS + +(define-private (read-rnd (block uint)) + (ok (lower-16-le (unwrap! (get-block-info? vrf-seed block) ERR_FAIL))) +) + +(define-private (lower-16-le (vrfSeed (buff 32))) + (+ + (lower-16-le-inner (element-at vrfSeed u16) u15) + (lower-16-le-inner (element-at vrfSeed u17) u14) + (lower-16-le-inner (element-at vrfSeed u18) u13) + (lower-16-le-inner (element-at vrfSeed u19) u12) + (lower-16-le-inner (element-at vrfSeed u20) u11) + (lower-16-le-inner (element-at vrfSeed u21) u10) + (lower-16-le-inner (element-at vrfSeed u22) u9) + (lower-16-le-inner (element-at vrfSeed u23) u8) + (lower-16-le-inner (element-at vrfSeed u24) u7) + (lower-16-le-inner (element-at vrfSeed u25) u6) + (lower-16-le-inner (element-at vrfSeed u26) u5) + (lower-16-le-inner (element-at vrfSeed u27) u4) + (lower-16-le-inner (element-at vrfSeed u28) u3) + (lower-16-le-inner (element-at vrfSeed u29) u2) + (lower-16-le-inner (element-at vrfSeed u30) u1) + (lower-16-le-inner (element-at vrfSeed u31) u0) + ) +) + +(define-private (lower-16-le-inner (byte (optional (buff 1))) (pos uint)) + (* (buff-to-u8 (unwrap-panic byte)) (pow u2 (* u8 pos))) +) + +(define-private (buff-to-u8 (byte (buff 1))) + (unwrap-panic (index-of 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff byte)) +) diff --git a/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-vrf-v2.json b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-vrf-v2.json new file mode 100644 index 00000000..89c8a70a --- /dev/null +++ b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-vrf-v2.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-auth-v2.clar b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-auth-v2.clar new file mode 100644 index 00000000..7c7379d2 --- /dev/null +++ b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-auth-v2.clar @@ -0,0 +1,630 @@ +;; NEWYORKCITYCOIN AUTH CONTRACT V2 +;; CityCoins Protocol Version 2.0.0 + +(define-constant CONTRACT_OWNER tx-sender) + +;; TRAIT DEFINITIONS + +(use-trait coreTraitV2 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-core-v2-trait.citycoin-core-v2) +(use-trait tokenTraitV2 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-token-v2-trait.citycoin-token-v2) + +;; ERRORS + +(define-constant ERR_UNKNOWN_JOB (err u6000)) +(define-constant ERR_UNAUTHORIZED (err u6001)) +(define-constant ERR_JOB_IS_ACTIVE (err u6002)) +(define-constant ERR_JOB_IS_NOT_ACTIVE (err u6003)) +(define-constant ERR_ALREADY_VOTED_THIS_WAY (err u6004)) +(define-constant ERR_JOB_IS_EXECUTED (err u6005)) +(define-constant ERR_JOB_IS_NOT_APPROVED (err u6006)) +(define-constant ERR_ARGUMENT_ALREADY_EXISTS (err u6007)) +(define-constant ERR_NO_ACTIVE_CORE_CONTRACT (err u6008)) +(define-constant ERR_CORE_CONTRACT_NOT_FOUND (err u6009)) +(define-constant ERR_UNKNOWN_ARGUMENT (err u6010)) +(define-constant ERR_INCORRECT_CONTRACT_STATE (err u6011)) +(define-constant ERR_CONTRACT_ALREADY_EXISTS (err u6012)) + +;; JOB MANAGEMENT + +(define-constant REQUIRED_APPROVALS u3) + +(define-data-var lastJobId uint u0) + +(define-map Jobs + uint + { + creator: principal, + name: (string-ascii 255), + target: principal, + approvals: uint, + disapprovals: uint, + isActive: bool, + isExecuted: bool + } +) + +(define-map JobApprovers + { jobId: uint, approver: principal } + bool +) + +(define-map Approvers + principal + bool +) + +(define-map ArgumentLastIdsByType + { jobId: uint, argumentType: (string-ascii 25) } + uint +) + +(define-map UIntArgumentsByName + { jobId: uint, argumentName: (string-ascii 255) } + { argumentId: uint, value: uint} +) + +(define-map UIntArgumentsById + { jobId: uint, argumentId: uint } + { argumentName: (string-ascii 255), value: uint } +) + +(define-map PrincipalArgumentsByName + { jobId: uint, argumentName: (string-ascii 255) } + { argumentId: uint, value: principal } +) + +(define-map PrincipalArgumentsById + { jobId: uint, argumentId: uint } + { argumentName: (string-ascii 255), value: principal } +) + +;; FUNCTIONS + +(define-read-only (get-last-job-id) + (var-get lastJobId) +) + +(define-public (create-job (name (string-ascii 255)) (target principal)) + (let + ( + (newJobId (+ (var-get lastJobId) u1)) + ) + (asserts! (is-approver tx-sender) ERR_UNAUTHORIZED) + (map-set Jobs + newJobId + { + creator: tx-sender, + name: name, + target: target, + approvals: u0, + disapprovals: u0, + isActive: false, + isExecuted: false + } + ) + (var-set lastJobId newJobId) + (ok newJobId) + ) +) + +(define-read-only (get-job (jobId uint)) + (map-get? Jobs jobId) +) + +(define-public (activate-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + ) + (asserts! (is-eq (get creator job) tx-sender) ERR_UNAUTHORIZED) + (asserts! (not (get isActive job)) ERR_JOB_IS_ACTIVE) + (map-set Jobs + jobId + (merge job { isActive: true }) + ) + (ok true) + ) +) + +(define-public (approve-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + (previousVote (map-get? JobApprovers { jobId: jobId, approver: tx-sender })) + ) + (asserts! (get isActive job) ERR_JOB_IS_NOT_ACTIVE) + (asserts! (is-approver tx-sender) ERR_UNAUTHORIZED) + ;; save vote + (map-set JobApprovers + { jobId: jobId, approver: tx-sender } + true + ) + (match previousVote approved + (begin + (asserts! (not approved) ERR_ALREADY_VOTED_THIS_WAY) + (map-set Jobs jobId + (merge job + { + approvals: (+ (get approvals job) u1), + disapprovals: (- (get disapprovals job) u1) + } + ) + ) + ) + ;; no previous vote + (map-set Jobs + jobId + (merge job { approvals: (+ (get approvals job) u1) } ) + ) + ) + (ok true) + ) +) + +(define-public (disapprove-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + (previousVote (map-get? JobApprovers { jobId: jobId, approver: tx-sender })) + ) + (asserts! (get isActive job) ERR_JOB_IS_NOT_ACTIVE) + (asserts! (is-approver tx-sender) ERR_UNAUTHORIZED) + ;; save vote + (map-set JobApprovers + { jobId: jobId, approver: tx-sender } + false + ) + (match previousVote approved + (begin + (asserts! approved ERR_ALREADY_VOTED_THIS_WAY) + (map-set Jobs jobId + (merge job + { + approvals: (- (get approvals job) u1), + disapprovals: (+ (get disapprovals job) u1) + } + ) + ) + ) + ;; no previous vote + (map-set Jobs + jobId + (merge job { disapprovals: (+ (get disapprovals job) u1) } ) + ) + ) + (ok true) + ) +) + +(define-read-only (is-job-approved (jobId uint)) + (match (get-job jobId) job + (>= (get approvals job) REQUIRED_APPROVALS) + false + ) +) + +(define-public (mark-job-as-executed (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + ) + (asserts! (get isActive job) ERR_JOB_IS_NOT_ACTIVE) + (asserts! (>= (get approvals job) REQUIRED_APPROVALS) ERR_JOB_IS_NOT_APPROVED) + (asserts! (is-eq (get target job) contract-caller) ERR_UNAUTHORIZED) + (asserts! (not (get isExecuted job)) ERR_JOB_IS_EXECUTED) + (map-set Jobs + jobId + (merge job { isExecuted: true }) + ) + (ok true) + ) +) + +(define-public (add-uint-argument (jobId uint) (argumentName (string-ascii 255)) (value uint)) + (let + ( + (argumentId (generate-argument-id jobId "uint")) + ) + (try! (guard-add-argument jobId)) + (asserts! + (and + (map-insert UIntArgumentsById + { jobId: jobId, argumentId: argumentId } + { argumentName: argumentName, value: value } + ) + (map-insert UIntArgumentsByName + { jobId: jobId, argumentName: argumentName } + { argumentId: argumentId, value: value} + ) + ) + ERR_ARGUMENT_ALREADY_EXISTS + ) + (ok true) + ) +) + +(define-read-only (get-uint-argument-by-name (jobId uint) (argumentName (string-ascii 255))) + (map-get? UIntArgumentsByName { jobId: jobId, argumentName: argumentName }) +) + +(define-read-only (get-uint-argument-by-id (jobId uint) (argumentId uint)) + (map-get? UIntArgumentsById { jobId: jobId, argumentId: argumentId }) +) + +(define-read-only (get-uint-value-by-name (jobId uint) (argumentName (string-ascii 255))) + (get value (get-uint-argument-by-name jobId argumentName)) +) + +(define-read-only (get-uint-value-by-id (jobId uint) (argumentId uint)) + (get value (get-uint-argument-by-id jobId argumentId)) +) + +(define-public (add-principal-argument (jobId uint) (argumentName (string-ascii 255)) (value principal)) + (let + ( + (argumentId (generate-argument-id jobId "principal")) + ) + (try! (guard-add-argument jobId)) + (asserts! + (and + (map-insert PrincipalArgumentsById + { jobId: jobId, argumentId: argumentId } + { argumentName: argumentName, value: value } + ) + (map-insert PrincipalArgumentsByName + { jobId: jobId, argumentName: argumentName } + { argumentId: argumentId, value: value} + ) + ) + ERR_ARGUMENT_ALREADY_EXISTS + ) + (ok true) + ) +) + +(define-read-only (get-principal-argument-by-name (jobId uint) (argumentName (string-ascii 255))) + (map-get? PrincipalArgumentsByName { jobId: jobId, argumentName: argumentName }) +) + +(define-read-only (get-principal-argument-by-id (jobId uint) (argumentId uint)) + (map-get? PrincipalArgumentsById { jobId: jobId, argumentId: argumentId }) +) + +(define-read-only (get-principal-value-by-name (jobId uint) (argumentName (string-ascii 255))) + (get value (get-principal-argument-by-name jobId argumentName)) +) + +(define-read-only (get-principal-value-by-id (jobId uint) (argumentId uint)) + (get value (get-principal-argument-by-id jobId argumentId)) +) + +;; PRIVATE FUNCTIONS + +(define-read-only (is-approver (user principal)) + (default-to false (map-get? Approvers user)) +) + +(define-private (generate-argument-id (jobId uint) (argumentType (string-ascii 25))) + (let + ( + (argumentId (+ (default-to u0 (map-get? ArgumentLastIdsByType { jobId: jobId, argumentType: argumentType })) u1)) + ) + (map-set ArgumentLastIdsByType + { jobId: jobId, argumentType: argumentType } + argumentId + ) + ;; return + argumentId + ) +) + +(define-private (guard-add-argument (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + ) + (asserts! (not (get isActive job)) ERR_JOB_IS_ACTIVE) + (asserts! (is-eq (get creator job) contract-caller) ERR_UNAUTHORIZED) + (ok true) + ) +) + +;; CONTRACT MANAGEMENT + +;; initial value for active core contract +;; set to deployer address at startup to prevent +;; circular dependency of core on auth +(define-data-var activeCoreContract principal CONTRACT_OWNER) +(define-data-var initialized bool false) + +;; core contract states +(define-constant STATE_DEPLOYED u0) +(define-constant STATE_ACTIVE u1) +(define-constant STATE_INACTIVE u2) + +;; core contract map +(define-map CoreContracts + principal + { + state: uint, + startHeight: uint, + endHeight: uint + } +) + +;; getter for active core contract +(define-read-only (get-active-core-contract) + (begin + (asserts! (not (is-eq (var-get activeCoreContract) CONTRACT_OWNER)) ERR_NO_ACTIVE_CORE_CONTRACT) + (ok (var-get activeCoreContract)) + ) +) + +;; getter for core contract map +(define-read-only (get-core-contract-info (targetContract principal)) + (let + ( + (coreContract (unwrap! (map-get? CoreContracts targetContract) ERR_CORE_CONTRACT_NOT_FOUND)) + ) + (ok coreContract) + ) +) + +;; one-time function to initialize contracts after all contracts are deployed +;; - check that deployer is calling this function +;; - check this contract is not activated already (one-time use) +;; - set initial map value for core contract v1 +;; - set cityWallet in core contract +;; - set intialized true +(define-public (initialize-contracts (coreContract )) + (let + ( + (coreContractAddress (contract-of coreContract)) + ) + (asserts! (is-eq contract-caller CONTRACT_OWNER) ERR_UNAUTHORIZED) + (asserts! (not (var-get initialized)) ERR_UNAUTHORIZED) + (map-set CoreContracts + coreContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (try! (contract-call? coreContract set-city-wallet (var-get cityWallet))) + (var-set initialized true) + (ok true) + ) +) + +(define-read-only (is-initialized) + (var-get initialized) +) + +;; function to activate core contract through registration +;; - check that target is in core contract map +;; - check that caller is core contract +;; - check that target is in STATE_DEPLOYED +;; - set active in core contract map +;; - set as activeCoreContract +(define-public (activate-core-contract (targetContract principal) (stacksHeight uint)) + (let + ( + (coreContract (unwrap! (map-get? CoreContracts targetContract) ERR_CORE_CONTRACT_NOT_FOUND)) + ) + (asserts! (is-eq (get state coreContract) STATE_DEPLOYED) ERR_INCORRECT_CONTRACT_STATE) + (asserts! (is-eq contract-caller targetContract) ERR_UNAUTHORIZED) + (map-set CoreContracts + targetContract + { + state: STATE_ACTIVE, + startHeight: stacksHeight, + endHeight: u0 + }) + (var-set activeCoreContract targetContract) + (ok true) + ) +) + +;; protected function to update core contract +(define-public (upgrade-core-contract (oldContract ) (newContract )) + (let + ( + (oldContractAddress (contract-of oldContract)) + (oldContractMap (unwrap! (map-get? CoreContracts oldContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + (newContractAddress (contract-of newContract)) + ) + (asserts! (not (is-eq oldContractAddress newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (is-none (map-get? CoreContracts newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + (map-set CoreContracts + oldContractAddress + { + state: STATE_INACTIVE, + startHeight: (get startHeight oldContractMap), + endHeight: block-height + }) + (map-set CoreContracts + newContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (var-set activeCoreContract newContractAddress) + (try! (contract-call? oldContract shutdown-contract block-height)) + (try! (contract-call? newContract set-city-wallet (var-get cityWallet))) + (ok true) + ) +) + +(define-public (execute-upgrade-core-contract-job (jobId uint) (oldContract ) (newContract )) + (let + ( + (oldContractArg (unwrap! (get-principal-value-by-name jobId "oldContract") ERR_UNKNOWN_ARGUMENT)) + (newContractArg (unwrap! (get-principal-value-by-name jobId "newContract") ERR_UNKNOWN_ARGUMENT)) + (oldContractAddress (contract-of oldContract)) + (oldContractMap (unwrap! (map-get? CoreContracts oldContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + (newContractAddress (contract-of newContract)) + ) + (asserts! (not (is-eq oldContractAddress newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (is-none (map-get? CoreContracts newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (and (is-eq oldContractArg oldContractAddress) (is-eq newContractArg newContractAddress)) ERR_UNAUTHORIZED) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (map-set CoreContracts + oldContractAddress + { + state: STATE_INACTIVE, + startHeight: (get startHeight oldContractMap), + endHeight: block-height + }) + (map-set CoreContracts + newContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (var-set activeCoreContract newContractAddress) + (try! (contract-call? oldContract shutdown-contract block-height)) + (try! (contract-call? newContract set-city-wallet (var-get cityWallet))) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; CITY WALLET MANAGEMENT + +;; initial value for city wallet +(define-data-var cityWallet principal 'SM18VBF2QYAAHN57Q28E2HSM15F6078JZYZ2FQBCX) + +;; returns city wallet principal +(define-read-only (get-city-wallet) + (ok (var-get cityWallet)) +) + +;; protected function to update city wallet variable +(define-public (set-city-wallet (targetContract ) (newCityWallet principal)) + (let + ( + (coreContractAddress (contract-of targetContract)) + (coreContract (unwrap! (map-get? CoreContracts coreContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + ) + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + (asserts! (is-eq coreContractAddress (var-get activeCoreContract)) ERR_UNAUTHORIZED) + (var-set cityWallet newCityWallet) + (try! (contract-call? targetContract set-city-wallet newCityWallet)) + (ok true) + ) +) + +(define-public (execute-set-city-wallet-job (jobId uint) (targetContract )) + (let + ( + (coreContractAddress (contract-of targetContract)) + (coreContract (unwrap! (map-get? CoreContracts coreContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + (newCityWallet (unwrap! (get-principal-value-by-name jobId "newCityWallet") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (asserts! (is-eq coreContractAddress (var-get activeCoreContract)) ERR_UNAUTHORIZED) + (var-set cityWallet newCityWallet) + (try! (contract-call? targetContract set-city-wallet newCityWallet)) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; check if contract caller is city wallet +(define-private (is-authorized-city) + (is-eq contract-caller (var-get cityWallet)) +) + +;; TOKEN MANAGEMENT + +(define-public (set-token-uri (targetContract ) (newUri (optional (string-utf8 256)))) + (begin + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + (as-contract (try! (contract-call? targetContract set-token-uri newUri))) + (ok true) + ) +) + +;; COINBASE THRESHOLDS + +(define-public (update-coinbase-thresholds (targetCore ) (targetToken ) (threshold1 uint) (threshold2 uint) (threshold3 uint) (threshold4 uint) (threshold5 uint)) + (begin + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + ;; update in token contract + (as-contract (try! (contract-call? targetToken update-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5))) + ;; update core contract based on token contract + (as-contract (try! (contract-call? targetCore update-coinbase-thresholds))) + (ok true) + ) +) + +(define-public (execute-update-coinbase-thresholds-job (jobId uint) (targetCore ) (targetToken )) + (let + ( + (threshold1 (unwrap! (get-uint-value-by-name jobId "threshold1") ERR_UNKNOWN_ARGUMENT)) + (threshold2 (unwrap! (get-uint-value-by-name jobId "threshold2") ERR_UNKNOWN_ARGUMENT)) + (threshold3 (unwrap! (get-uint-value-by-name jobId "threshold3") ERR_UNKNOWN_ARGUMENT)) + (threshold4 (unwrap! (get-uint-value-by-name jobId "threshold4") ERR_UNKNOWN_ARGUMENT)) + (threshold5 (unwrap! (get-uint-value-by-name jobId "threshold5") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (as-contract (try! (contract-call? targetToken update-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5))) + (as-contract (try! (contract-call? targetCore update-coinbase-thresholds))) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; COINBASE AMOUNTS (REWARDS) + +(define-public (update-coinbase-amounts (targetCore ) (targetToken ) (amountBonus uint) (amount1 uint) (amount2 uint) (amount3 uint) (amount4 uint) (amount5 uint) (amountDefault uint)) + (begin + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + ;; update in token contract + (as-contract (try! (contract-call? targetToken update-coinbase-amounts amountBonus amount1 amount2 amount3 amount4 amount5 amountDefault))) + ;; update core contract based on token contract + (as-contract (try! (contract-call? targetCore update-coinbase-amounts))) + (ok true) + ) +) + +(define-public (execute-update-coinbase-amounts-job (jobId uint) (targetCore ) (targetToken )) + (let + ( + (amountBonus (unwrap! (get-uint-value-by-name jobId "amountBonus") ERR_UNKNOWN_ARGUMENT)) + (amount1 (unwrap! (get-uint-value-by-name jobId "amount1") ERR_UNKNOWN_ARGUMENT)) + (amount2 (unwrap! (get-uint-value-by-name jobId "amount2") ERR_UNKNOWN_ARGUMENT)) + (amount3 (unwrap! (get-uint-value-by-name jobId "amount3") ERR_UNKNOWN_ARGUMENT)) + (amount4 (unwrap! (get-uint-value-by-name jobId "amount4") ERR_UNKNOWN_ARGUMENT)) + (amount5 (unwrap! (get-uint-value-by-name jobId "amount5") ERR_UNKNOWN_ARGUMENT)) + (amountDefault (unwrap! (get-uint-value-by-name jobId "amountDefault") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (as-contract (try! (contract-call? targetToken update-coinbase-amounts amountBonus amount1 amount2 amount3 amount4 amount5 amountDefault))) + (as-contract (try! (contract-call? targetCore update-coinbase-amounts))) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; APPROVERS MANAGEMENT + +(define-public (execute-replace-approver-job (jobId uint)) + (let + ( + (oldApprover (unwrap! (get-principal-value-by-name jobId "oldApprover") ERR_UNKNOWN_ARGUMENT)) + (newApprover (unwrap! (get-principal-value-by-name jobId "newApprover") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (map-set Approvers oldApprover false) + (map-set Approvers newApprover true) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; CONTRACT INITIALIZATION + +(map-insert Approvers 'SP372JVX6EWE2M0XPA84MWZYRRG2M6CAC4VVC12V1 true) +(map-insert Approvers 'SP2R0DQYR7XHD161SH2GK49QRP1YSV7HE9JSG7W6G true) +(map-insert Approvers 'SPN4Y5QPGQA8882ZXW90ADC2DHYXMSTN8VAR8C3X true) +(map-insert Approvers 'SP3YYGCGX1B62CYAH4QX7PQE63YXG7RDTXD8BQHJQ true) +(map-insert Approvers 'SP7DGES13508FHRWS1FB0J3SZA326FP6QRMB6JDE true) diff --git a/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-auth-v2.json b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-auth-v2.json new file mode 100644 index 00000000..89c8a70a --- /dev/null +++ b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-auth-v2.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-core-v2.clar b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-core-v2.clar new file mode 100644 index 00000000..831a993d --- /dev/null +++ b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-core-v2.clar @@ -0,0 +1,1008 @@ +;; NEWYORKCITYCOIN CORE CONTRACT V2 +;; CityCoins Protocol Version 2.0.0 + +;; GENERAL CONFIGURATION + +(impl-trait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.citycoin-core) +(impl-trait 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-core-v2-trait.citycoin-core-v2) +(define-constant CONTRACT_OWNER tx-sender) + +;; ERROR CODES + +(define-constant ERR_UNAUTHORIZED (err u1000)) +(define-constant ERR_USER_ALREADY_REGISTERED (err u1001)) +(define-constant ERR_USER_NOT_FOUND (err u1002)) +(define-constant ERR_USER_ID_NOT_FOUND (err u1003)) +(define-constant ERR_ACTIVATION_THRESHOLD_REACHED (err u1004)) +(define-constant ERR_CONTRACT_NOT_ACTIVATED (err u1005)) +(define-constant ERR_USER_ALREADY_MINED (err u1006)) +(define-constant ERR_INSUFFICIENT_COMMITMENT (err u1007)) +(define-constant ERR_INSUFFICIENT_BALANCE (err u1008)) +(define-constant ERR_USER_DID_NOT_MINE_IN_BLOCK (err u1009)) +(define-constant ERR_CLAIMED_BEFORE_MATURITY (err u1010)) +(define-constant ERR_NO_MINERS_AT_BLOCK (err u1011)) +(define-constant ERR_REWARD_ALREADY_CLAIMED (err u1012)) +(define-constant ERR_MINER_DID_NOT_WIN (err u1013)) +(define-constant ERR_NO_VRF_SEED_FOUND (err u1014)) +(define-constant ERR_STACKING_NOT_AVAILABLE (err u1015)) +(define-constant ERR_CANNOT_STACK (err u1016)) +(define-constant ERR_REWARD_CYCLE_NOT_COMPLETED (err u1017)) +(define-constant ERR_NOTHING_TO_REDEEM (err u1018)) +(define-constant ERR_UNABLE_TO_FIND_CITY_WALLET (err u1019)) +(define-constant ERR_CLAIM_IN_WRONG_CONTRACT (err u1020)) +(define-constant ERR_BLOCK_HEIGHT_IN_PAST (err u1021)) +(define-constant ERR_COINBASE_AMOUNTS_NOT_FOUND (err u1022)) + +;; CITY WALLET MANAGEMENT + +;; initial value for city wallet, set to this contract until initialized +(define-data-var cityWallet principal 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-core-v2) + +;; returns set city wallet principal +(define-read-only (get-city-wallet) + (var-get cityWallet) +) + +;; protected function to update city wallet variable +(define-public (set-city-wallet (newCityWallet principal)) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (ok (var-set cityWallet newCityWallet)) + ) +) + +;; REGISTRATION + +(define-constant NEWYORKCITYCOIN_ACTIVATION_HEIGHT u37449) +(define-data-var activationBlock uint u340282366920938463463374607431768211455) +(define-data-var activationDelay uint u0) +(define-data-var activationReached bool false) +(define-data-var activationTarget uint u0) +(define-data-var activationThreshold uint u20) +(define-data-var usersNonce uint u0) + +;; returns Stacks block height registration was activated at plus activationDelay +(define-read-only (get-activation-block) + (begin + (asserts! (get-activation-status) ERR_CONTRACT_NOT_ACTIVATED) + (ok (var-get activationBlock)) + ) +) + +;; returns activation delay +(define-read-only (get-activation-delay) + (var-get activationDelay) +) + +;; returns activation status as boolean +(define-read-only (get-activation-status) + (var-get activationReached) +) + +;; returns activation target +(define-read-only (get-activation-target) + (begin + (asserts! (get-activation-status) ERR_CONTRACT_NOT_ACTIVATED) + (ok (var-get activationTarget)) + ) +) + +;; returns activation threshold +(define-read-only (get-activation-threshold) + (var-get activationThreshold) +) + +;; returns number of registered users, used for activation and tracking user IDs +(define-read-only (get-registered-users-nonce) + (var-get usersNonce) +) + +;; store user principal by user id +(define-map Users + uint + principal +) + +;; store user id by user principal +(define-map UserIds + principal + uint +) + +;; returns (some userId) or none +(define-read-only (get-user-id (user principal)) + (map-get? UserIds user) +) + +;; returns (some userPrincipal) or none +(define-read-only (get-user (userId uint)) + (map-get? Users userId) +) + +;; returns user ID if it has been created, or creates and returns new ID +(define-private (get-or-create-user-id (user principal)) + (match + (map-get? UserIds user) + value value + (let + ( + (newId (+ u1 (var-get usersNonce))) + ) + (map-set Users newId user) + (map-set UserIds user newId) + (var-set usersNonce newId) + newId + ) + ) +) + +;; registers users that signal activation of contract until threshold is met +(define-public (register-user (memo (optional (string-utf8 50)))) + (let + ( + (newId (+ u1 (var-get usersNonce))) + (threshold (var-get activationThreshold)) + (initialized (contract-call? 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-auth-v2 is-initialized)) + ) + + (asserts! initialized ERR_UNAUTHORIZED) + + (asserts! (is-none (map-get? UserIds tx-sender)) + ERR_USER_ALREADY_REGISTERED) + + (asserts! (<= newId threshold) + ERR_ACTIVATION_THRESHOLD_REACHED) + + (if (is-some memo) + (print memo) + none + ) + + (get-or-create-user-id tx-sender) + + (if (is-eq newId threshold) + (let + ( + (activationTargetBlock (+ block-height (var-get activationDelay))) + ) + (try! (contract-call? 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-auth-v2 activate-core-contract (as-contract tx-sender) activationTargetBlock)) + (try! (contract-call? 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 activate-token (as-contract tx-sender) NEWYORKCITYCOIN_ACTIVATION_HEIGHT)) + (try! (set-coinbase-thresholds)) + (try! (set-coinbase-amounts)) + (var-set activationReached true) + (var-set activationBlock NEWYORKCITYCOIN_ACTIVATION_HEIGHT) + (var-set activationTarget activationTargetBlock) + (ok true) + ) + (ok true) + ) + ) +) + +;; MINING CONFIGURATION + +;; define split to custodied wallet address for the city +(define-constant SPLIT_CITY_PCT u30) + +;; how long a miner must wait before block winner can claim their minted tokens +(define-data-var tokenRewardMaturity uint u100) + +;; At a given Stacks block height: +;; - how many miners were there +;; - what was the total amount submitted +;; - what was the total amount submitted to the city +;; - what was the total amount submitted to Stackers +;; - was the block reward claimed +(define-map MiningStatsAtBlock + uint + { + minersCount: uint, + amount: uint, + amountToCity: uint, + amountToStackers: uint, + rewardClaimed: bool + } +) + +;; returns map MiningStatsAtBlock at a given Stacks block height if it exists +(define-read-only (get-mining-stats-at-block (stacksHeight uint)) + (map-get? MiningStatsAtBlock stacksHeight) +) + +;; returns map MiningStatsAtBlock at a given Stacks block height +;; or, an empty structure +(define-read-only (get-mining-stats-at-block-or-default (stacksHeight uint)) + (default-to { + minersCount: u0, + amount: u0, + amountToCity: u0, + amountToStackers: u0, + rewardClaimed: false + } + (map-get? MiningStatsAtBlock stacksHeight) + ) +) + +;; At a given Stacks block height and user ID: +;; - what is their ustx commitment +;; - what are the low/high values (used for VRF) +(define-map MinersAtBlock + { + stacksHeight: uint, + userId: uint + } + { + ustx: uint, + lowValue: uint, + highValue: uint, + winner: bool + } +) + +;; returns true if a given miner has already mined at a given block height +(define-read-only (has-mined-at-block (stacksHeight uint) (userId uint)) + (is-some + (map-get? MinersAtBlock { stacksHeight: stacksHeight, userId: userId }) + ) +) + +;; returns map MinersAtBlock at a given Stacks block height for a user ID +(define-read-only (get-miner-at-block (stacksHeight uint) (userId uint)) + (map-get? MinersAtBlock { stacksHeight: stacksHeight, userId: userId }) +) + +;; returns map MinersAtBlock at a given Stacks block height for a user ID +;; or, an empty structure +(define-read-only (get-miner-at-block-or-default (stacksHeight uint) (userId uint)) + (default-to { + highValue: u0, + lowValue: u0, + ustx: u0, + winner: false + } + (map-get? MinersAtBlock { stacksHeight: stacksHeight, userId: userId })) +) + +;; At a given Stacks block height: +;; - what is the max highValue from MinersAtBlock (used for VRF) +(define-map MinersAtBlockHighValue + uint + uint +) + +;; returns last high value from map MinersAtBlockHighValue +(define-read-only (get-last-high-value-at-block (stacksHeight uint)) + (default-to u0 + (map-get? MinersAtBlockHighValue stacksHeight)) +) + +;; At a given Stacks block height: +;; - what is the userId of miner who won this block +(define-map BlockWinnerIds + uint + uint +) + +(define-read-only (get-block-winner-id (stacksHeight uint)) + (map-get? BlockWinnerIds stacksHeight) +) + +;; MINING ACTIONS + +(define-public (mine-tokens (amountUstx uint) (memo (optional (buff 34)))) + (let + ( + (userId (get-or-create-user-id tx-sender)) + ) + (try! (mine-tokens-at-block userId block-height amountUstx memo)) + (ok true) + ) +) + +(define-public (mine-many (amounts (list 200 uint))) + (begin + (asserts! (is-activated) ERR_CONTRACT_NOT_ACTIVATED) + (asserts! (> (len amounts) u0) ERR_INSUFFICIENT_COMMITMENT) + (match (fold mine-single amounts (ok { userId: (get-or-create-user-id tx-sender), toStackers: u0, toCity: u0, stacksHeight: block-height })) + okReturn + (begin + (asserts! (>= (stx-get-balance tx-sender) (+ (get toStackers okReturn) (get toCity okReturn))) ERR_INSUFFICIENT_BALANCE) + (if (> (get toStackers okReturn ) u0) + (try! (stx-transfer? (get toStackers okReturn ) tx-sender (as-contract tx-sender))) + false + ) + (try! (stx-transfer? (get toCity okReturn) tx-sender (var-get cityWallet))) + (print { + firstBlock: block-height, + lastBlock: (- (+ block-height (len amounts)) u1) + }) + (ok true) + ) + errReturn (err errReturn) + ) + ) +) + +(define-private (mine-single + (amountUstx uint) + (return (response + { + userId: uint, + toStackers: uint, + toCity: uint, + stacksHeight: uint + } + uint + ))) + + (match return okReturn + (let + ( + (stacksHeight (get stacksHeight okReturn)) + (rewardCycle (default-to u0 (get-reward-cycle stacksHeight))) + (stackingActive (stacking-active-at-cycle rewardCycle)) + (toCity + (if stackingActive + (/ (* SPLIT_CITY_PCT amountUstx) u100) + amountUstx + ) + ) + (toStackers (- amountUstx toCity)) + ) + (asserts! (not (has-mined-at-block stacksHeight (get userId okReturn))) ERR_USER_ALREADY_MINED) + (asserts! (> amountUstx u0) ERR_INSUFFICIENT_COMMITMENT) + (try! (set-tokens-mined (get userId okReturn) stacksHeight amountUstx toStackers toCity)) + (ok (merge okReturn + { + toStackers: (+ (get toStackers okReturn) toStackers), + toCity: (+ (get toCity okReturn) toCity), + stacksHeight: (+ stacksHeight u1) + } + )) + ) + errReturn (err errReturn) + ) +) + +(define-private (mine-tokens-at-block (userId uint) (stacksHeight uint) (amountUstx uint) (memo (optional (buff 34)))) + (let + ( + (rewardCycle (default-to u0 (get-reward-cycle stacksHeight))) + (stackingActive (stacking-active-at-cycle rewardCycle)) + (toCity + (if stackingActive + (/ (* SPLIT_CITY_PCT amountUstx) u100) + amountUstx + ) + ) + (toStackers (- amountUstx toCity)) + ) + (asserts! (is-activated) ERR_CONTRACT_NOT_ACTIVATED) + (asserts! (not (has-mined-at-block stacksHeight userId)) ERR_USER_ALREADY_MINED) + (asserts! (> amountUstx u0) ERR_INSUFFICIENT_COMMITMENT) + (asserts! (>= (stx-get-balance tx-sender) amountUstx) ERR_INSUFFICIENT_BALANCE) + (try! (set-tokens-mined userId stacksHeight amountUstx toStackers toCity)) + (if (is-some memo) + (print memo) + none + ) + (if stackingActive + (try! (stx-transfer? toStackers tx-sender (as-contract tx-sender))) + false + ) + (try! (stx-transfer? toCity tx-sender (var-get cityWallet))) + (ok true) + ) +) + +(define-private (set-tokens-mined (userId uint) (stacksHeight uint) (amountUstx uint) (toStackers uint) (toCity uint)) + (let + ( + (blockStats (get-mining-stats-at-block-or-default stacksHeight)) + (newMinersCount (+ (get minersCount blockStats) u1)) + (minerLowVal (get-last-high-value-at-block stacksHeight)) + (rewardCycle (unwrap! (get-reward-cycle stacksHeight) + ERR_STACKING_NOT_AVAILABLE)) + (rewardCycleStats (get-stacking-stats-at-cycle-or-default rewardCycle)) + ) + (map-set MiningStatsAtBlock + stacksHeight + { + minersCount: newMinersCount, + amount: (+ (get amount blockStats) amountUstx), + amountToCity: (+ (get amountToCity blockStats) toCity), + amountToStackers: (+ (get amountToStackers blockStats) toStackers), + rewardClaimed: false + } + ) + (map-set MinersAtBlock + { + stacksHeight: stacksHeight, + userId: userId + } + { + ustx: amountUstx, + lowValue: (if (> minerLowVal u0) (+ minerLowVal u1) u0), + highValue: (+ minerLowVal amountUstx), + winner: false + } + ) + (map-set MinersAtBlockHighValue + stacksHeight + (+ minerLowVal amountUstx) + ) + (if (> toStackers u0) + (map-set StackingStatsAtCycle + rewardCycle + { + amountUstx: (+ (get amountUstx rewardCycleStats) toStackers), + amountToken: (get amountToken rewardCycleStats) + } + ) + false + ) + (ok true) + ) +) + +;; MINING REWARD CLAIM ACTIONS + +;; calls function to claim mining reward in active logic contract +(define-public (claim-mining-reward (minerBlockHeight uint)) + (begin + (asserts! (or (is-eq (var-get shutdownHeight) u0) (< minerBlockHeight (var-get shutdownHeight))) ERR_CLAIM_IN_WRONG_CONTRACT) + (try! (claim-mining-reward-at-block tx-sender block-height minerBlockHeight)) + (ok true) + ) +) + +;; Determine whether or not the given principal can claim the mined tokens at a particular block height, +;; given the miners record for that block height, a random sample, and the current block height. +(define-private (claim-mining-reward-at-block (user principal) (stacksHeight uint) (minerBlockHeight uint)) + (let + ( + (maturityHeight (+ (var-get tokenRewardMaturity) minerBlockHeight)) + (userId (unwrap! (get-user-id user) ERR_USER_ID_NOT_FOUND)) + (blockStats (unwrap! (get-mining-stats-at-block minerBlockHeight) ERR_NO_MINERS_AT_BLOCK)) + (minerStats (unwrap! (get-miner-at-block minerBlockHeight userId) ERR_USER_DID_NOT_MINE_IN_BLOCK)) + (isMature (asserts! (> stacksHeight maturityHeight) ERR_CLAIMED_BEFORE_MATURITY)) + (vrfSample (unwrap! (contract-call? 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-vrf-v2 get-save-rnd maturityHeight) ERR_NO_VRF_SEED_FOUND)) + (commitTotal (get-last-high-value-at-block minerBlockHeight)) + (winningValue (mod vrfSample commitTotal)) + ) + (asserts! (not (get rewardClaimed blockStats)) ERR_REWARD_ALREADY_CLAIMED) + (asserts! (and (>= winningValue (get lowValue minerStats)) (<= winningValue (get highValue minerStats))) + ERR_MINER_DID_NOT_WIN) + (try! (set-mining-reward-claimed userId minerBlockHeight)) + (ok true) + ) +) + +(define-private (set-mining-reward-claimed (userId uint) (minerBlockHeight uint)) + (let + ( + (blockStats (get-mining-stats-at-block-or-default minerBlockHeight)) + (minerStats (get-miner-at-block-or-default minerBlockHeight userId)) + (user (unwrap! (get-user userId) ERR_USER_NOT_FOUND)) + ) + (map-set MiningStatsAtBlock + minerBlockHeight + { + minersCount: (get minersCount blockStats), + amount: (get amount blockStats), + amountToCity: (get amountToCity blockStats), + amountToStackers: (get amountToStackers blockStats), + rewardClaimed: true + } + ) + (map-set MinersAtBlock + { + stacksHeight: minerBlockHeight, + userId: userId + } + { + ustx: (get ustx minerStats), + lowValue: (get lowValue minerStats), + highValue: (get highValue minerStats), + winner: true + } + ) + (map-set BlockWinnerIds + minerBlockHeight + userId + ) + (try! (mint-coinbase user minerBlockHeight)) + (ok true) + ) +) + +(define-read-only (is-block-winner (user principal) (minerBlockHeight uint)) + (is-block-winner-and-can-claim user minerBlockHeight false) +) + +(define-read-only (can-claim-mining-reward (user principal) (minerBlockHeight uint)) + (is-block-winner-and-can-claim user minerBlockHeight true) +) + +(define-private (is-block-winner-and-can-claim (user principal) (minerBlockHeight uint) (testCanClaim bool)) + (let + ( + (userId (unwrap! (get-user-id user) false)) + (blockStats (unwrap! (get-mining-stats-at-block minerBlockHeight) false)) + (minerStats (unwrap! (get-miner-at-block minerBlockHeight userId) false)) + (maturityHeight (+ (var-get tokenRewardMaturity) minerBlockHeight)) + (vrfSample (unwrap! (contract-call? 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-vrf-v2 get-rnd maturityHeight) false)) + (commitTotal (get-last-high-value-at-block minerBlockHeight)) + (winningValue (mod vrfSample commitTotal)) + ) + (if (and (>= winningValue (get lowValue minerStats)) (<= winningValue (get highValue minerStats))) + (if testCanClaim (not (get rewardClaimed blockStats)) true) + false + ) + ) +) + +;; STACKING CONFIGURATION + +(define-constant MAX_REWARD_CYCLES u32) +(define-constant REWARD_CYCLE_INDEXES (list u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11 u12 u13 u14 u15 u16 u17 u18 u19 u20 u21 u22 u23 u24 u25 u26 u27 u28 u29 u30 u31)) + +;; how long a reward cycle is +(define-data-var rewardCycleLength uint u2100) + +;; At a given reward cycle: +;; - how many Stackers were there +;; - what is the total uSTX submitted by miners +;; - what is the total amount of tokens stacked +(define-map StackingStatsAtCycle + uint + { + amountUstx: uint, + amountToken: uint + } +) + +;; returns the total stacked tokens and committed uSTX for a given reward cycle +(define-read-only (get-stacking-stats-at-cycle (rewardCycle uint)) + (map-get? StackingStatsAtCycle rewardCycle) +) + +;; returns the total stacked tokens and committed uSTX for a given reward cycle +;; or, an empty structure +(define-read-only (get-stacking-stats-at-cycle-or-default (rewardCycle uint)) + (default-to { amountUstx: u0, amountToken: u0 } + (map-get? StackingStatsAtCycle rewardCycle)) +) + +;; At a given reward cycle and user ID: +;; - what is the total tokens Stacked? +;; - how many tokens should be returned? (based on Stacking period) +(define-map StackerAtCycle + { + rewardCycle: uint, + userId: uint + } + { + amountStacked: uint, + toReturn: uint + } +) + +(define-read-only (get-stacker-at-cycle (rewardCycle uint) (userId uint)) + (map-get? StackerAtCycle { rewardCycle: rewardCycle, userId: userId }) +) + +(define-read-only (get-stacker-at-cycle-or-default (rewardCycle uint) (userId uint)) + (default-to { amountStacked: u0, toReturn: u0 } + (map-get? StackerAtCycle { rewardCycle: rewardCycle, userId: userId })) +) + +;; get the reward cycle for a given Stacks block height +(define-read-only (get-reward-cycle (stacksHeight uint)) + (let + ( + (firstStackingBlock (var-get activationBlock)) + (rcLen (var-get rewardCycleLength)) + ) + (if (>= stacksHeight firstStackingBlock) + (some (/ (- stacksHeight firstStackingBlock) rcLen)) + none) + ) +) + +;; determine if stacking is active in a given cycle +(define-read-only (stacking-active-at-cycle (rewardCycle uint)) + (is-some + (get amountToken (map-get? StackingStatsAtCycle rewardCycle)) + ) +) + +;; get the first Stacks block height for a given reward cycle. +(define-read-only (get-first-stacks-block-in-reward-cycle (rewardCycle uint)) + (+ (var-get activationBlock) (* (var-get rewardCycleLength) rewardCycle)) +) + +;; getter for get-entitled-stacking-reward that specifies block height +(define-read-only (get-stacking-reward (userId uint) (targetCycle uint)) + (get-entitled-stacking-reward userId targetCycle block-height) +) + +;; get uSTX a Stacker can claim, given reward cycle they stacked in and current block height +;; this method only returns a positive value if: +;; - the current block height is in a subsequent reward cycle +;; - the stacker actually locked up tokens in the target reward cycle +;; - the stacker locked up _enough_ tokens to get at least one uSTX +;; it is possible to Stack tokens and not receive uSTX: +;; - if no miners commit during this reward cycle +;; - the amount stacked by user is too few that you'd be entitled to less than 1 uSTX +(define-private (get-entitled-stacking-reward (userId uint) (targetCycle uint) (stacksHeight uint)) + (let + ( + (rewardCycleStats (get-stacking-stats-at-cycle-or-default targetCycle)) + (stackerAtCycle (get-stacker-at-cycle-or-default targetCycle userId)) + (totalUstxThisCycle (get amountUstx rewardCycleStats)) + (totalStackedThisCycle (get amountToken rewardCycleStats)) + (userStackedThisCycle (get amountStacked stackerAtCycle)) + ) + (match (get-reward-cycle stacksHeight) + currentCycle + (if (and (not (var-get isShutdown)) + (or (<= currentCycle targetCycle) (is-eq u0 userStackedThisCycle))) + ;; the contract is not shut down and + ;; this cycle hasn't finished + ;; or stacker contributed nothing + u0 + ;; (totalUstxThisCycle * userStackedThisCycle) / totalStackedThisCycle + (/ (* totalUstxThisCycle userStackedThisCycle) totalStackedThisCycle) + ) + ;; before first reward cycle + u0 + ) + ) +) + +;; STACKING ACTIONS + +(define-public (stack-tokens (amountTokens uint) (lockPeriod uint)) + (let + ( + (userId (get-or-create-user-id tx-sender)) + ) + (try! (stack-tokens-at-cycle tx-sender userId amountTokens block-height lockPeriod)) + (ok true) + ) +) + +(define-private (stack-tokens-at-cycle (user principal) (userId uint) (amountTokens uint) (startHeight uint) (lockPeriod uint)) + (let + ( + (currentCycle (unwrap! (get-reward-cycle startHeight) ERR_STACKING_NOT_AVAILABLE)) + (targetCycle (+ u1 currentCycle)) + (commitment { + stackerId: userId, + amount: amountTokens, + first: targetCycle, + last: (+ targetCycle lockPeriod) + }) + ) + (asserts! (is-activated) ERR_CONTRACT_NOT_ACTIVATED) + (asserts! (and (> lockPeriod u0) (<= lockPeriod MAX_REWARD_CYCLES)) + ERR_CANNOT_STACK) + (asserts! (> amountTokens u0) ERR_CANNOT_STACK) + (try! (contract-call? 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 transfer amountTokens tx-sender (as-contract tx-sender) none)) + (print { + firstCycle: targetCycle, + lastCycle: (- (+ targetCycle lockPeriod) u1) + }) + (match (fold stack-tokens-closure REWARD_CYCLE_INDEXES (ok commitment)) + okValue (ok true) + errValue (err errValue) + ) + ) +) + +(define-private (stack-tokens-closure (rewardCycleIdx uint) + (commitmentResponse (response + { + stackerId: uint, + amount: uint, + first: uint, + last: uint + } + uint + ))) + + (match commitmentResponse + commitment + (let + ( + (stackerId (get stackerId commitment)) + (amountToken (get amount commitment)) + (firstCycle (get first commitment)) + (lastCycle (get last commitment)) + (targetCycle (+ firstCycle rewardCycleIdx)) + ) + (begin + (if (and (>= targetCycle firstCycle) (< targetCycle lastCycle)) + (begin + (if (is-eq targetCycle (- lastCycle u1)) + (set-tokens-stacked stackerId targetCycle amountToken amountToken) + (set-tokens-stacked stackerId targetCycle amountToken u0) + ) + true + ) + false + ) + commitmentResponse + ) + ) + errValue commitmentResponse + ) +) + +(define-private (set-tokens-stacked (userId uint) (targetCycle uint) (amountStacked uint) (toReturn uint)) + (let + ( + (rewardCycleStats (get-stacking-stats-at-cycle-or-default targetCycle)) + (stackerAtCycle (get-stacker-at-cycle-or-default targetCycle userId)) + ) + (map-set StackingStatsAtCycle + targetCycle + { + amountUstx: (get amountUstx rewardCycleStats), + amountToken: (+ amountStacked (get amountToken rewardCycleStats)) + } + ) + (map-set StackerAtCycle + { + rewardCycle: targetCycle, + userId: userId + } + { + amountStacked: (+ amountStacked (get amountStacked stackerAtCycle)), + toReturn: (+ toReturn (get toReturn stackerAtCycle)) + } + ) + ) +) + +;; STACKING REWARD CLAIMS + +;; calls function to claim stacking reward in active logic contract +(define-public (claim-stacking-reward (targetCycle uint)) + (begin + (try! (claim-stacking-reward-at-cycle tx-sender block-height targetCycle)) + (ok true) + ) +) + +(define-private (claim-stacking-reward-at-cycle (user principal) (stacksHeight uint) (targetCycle uint)) + (let + ( + (currentCycle (unwrap! (get-reward-cycle stacksHeight) ERR_STACKING_NOT_AVAILABLE)) + (userId (unwrap! (get-user-id user) ERR_USER_ID_NOT_FOUND)) + (entitledUstx (get-entitled-stacking-reward userId targetCycle stacksHeight)) + (stackerAtCycle (get-stacker-at-cycle-or-default targetCycle userId)) + (toReturn (get toReturn stackerAtCycle)) + ) + (asserts! (or + (is-eq true (var-get isShutdown)) + (> currentCycle targetCycle)) + ERR_REWARD_CYCLE_NOT_COMPLETED) + (asserts! (or (> toReturn u0) (> entitledUstx u0)) ERR_NOTHING_TO_REDEEM) + ;; disable ability to claim again + (map-set StackerAtCycle + { + rewardCycle: targetCycle, + userId: userId + } + { + amountStacked: u0, + toReturn: u0 + } + ) + ;; send back tokens if user was eligible + (if (> toReturn u0) + (try! (as-contract (contract-call? 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 transfer toReturn tx-sender user none))) + true + ) + ;; send back rewards if user was eligible + (if (> entitledUstx u0) + (try! (as-contract (stx-transfer? entitledUstx tx-sender user))) + true + ) + (ok true) + ) +) + +;; TOKEN CONFIGURATION + +;; decimals and multiplier for token +(define-constant DECIMALS u6) +(define-constant MICRO_CITYCOINS (pow u10 DECIMALS)) + +;; bonus period length for increased coinbase rewards +(define-constant TOKEN_BONUS_PERIOD u10000) + +;; coinbase thresholds per halving, used to determine halvings +(define-data-var coinbaseThreshold1 uint u0) +(define-data-var coinbaseThreshold2 uint u0) +(define-data-var coinbaseThreshold3 uint u0) +(define-data-var coinbaseThreshold4 uint u0) +(define-data-var coinbaseThreshold5 uint u0) + +;; return coinbase thresholds if contract activated +(define-read-only (get-coinbase-thresholds) + (let + ( + (activated (get-activation-status)) + ) + (asserts! activated ERR_CONTRACT_NOT_ACTIVATED) + (ok { + coinbaseThreshold1: (var-get coinbaseThreshold1), + coinbaseThreshold2: (var-get coinbaseThreshold2), + coinbaseThreshold3: (var-get coinbaseThreshold3), + coinbaseThreshold4: (var-get coinbaseThreshold4), + coinbaseThreshold5: (var-get coinbaseThreshold5) + }) + ) +) + +;; set coinbase thresholds, used during activation +(define-private (set-coinbase-thresholds) + (let + ( + (coinbaseThresholds (try! (contract-call? 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 get-coinbase-thresholds))) + ) + (var-set coinbaseThreshold1 (get coinbaseThreshold1 coinbaseThresholds)) + (var-set coinbaseThreshold2 (get coinbaseThreshold2 coinbaseThresholds)) + (var-set coinbaseThreshold3 (get coinbaseThreshold3 coinbaseThresholds)) + (var-set coinbaseThreshold4 (get coinbaseThreshold4 coinbaseThresholds)) + (var-set coinbaseThreshold5 (get coinbaseThreshold5 coinbaseThresholds)) + ;; print coinbase thresholds + (print { + coinbaseThreshold1: (var-get coinbaseThreshold1), + coinbaseThreshold2: (var-get coinbaseThreshold2), + coinbaseThreshold3: (var-get coinbaseThreshold3), + coinbaseThreshold4: (var-get coinbaseThreshold4), + coinbaseThreshold5: (var-get coinbaseThreshold5) + }) + (ok true) + ) +) + +;; guarded function for auth to update coinbase thresholds +(define-public (update-coinbase-thresholds) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (try! (set-coinbase-thresholds)) + (ok true) + ) +) + +;; coinbase rewards per threshold, used to determine rewards +(define-data-var coinbaseAmountBonus uint u0) +(define-data-var coinbaseAmount1 uint u0) +(define-data-var coinbaseAmount2 uint u0) +(define-data-var coinbaseAmount3 uint u0) +(define-data-var coinbaseAmount4 uint u0) +(define-data-var coinbaseAmount5 uint u0) +(define-data-var coinbaseAmountDefault uint u0) + +;; return coinbase amounts if contract activated +(define-read-only (get-coinbase-amounts) + (let + ( + (activated (get-activation-status)) + ) + (asserts! activated ERR_CONTRACT_NOT_ACTIVATED) + (ok { + coinbaseAmountBonus: (var-get coinbaseAmountBonus), + coinbaseAmount1: (var-get coinbaseAmount1), + coinbaseAmount2: (var-get coinbaseAmount2), + coinbaseAmount3: (var-get coinbaseAmount3), + coinbaseAmount4: (var-get coinbaseAmount4), + coinbaseAmount5: (var-get coinbaseAmount5), + coinbaseAmountDefault: (var-get coinbaseAmountDefault) + }) + ) +) + +;; set coinbase amounts, used during activation +(define-private (set-coinbase-amounts) + (let + ( + (coinbaseAmounts (unwrap! (contract-call? 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 get-coinbase-amounts) ERR_COINBASE_AMOUNTS_NOT_FOUND)) + ) + (var-set coinbaseAmountBonus (get coinbaseAmountBonus coinbaseAmounts)) + (var-set coinbaseAmount1 (get coinbaseAmount1 coinbaseAmounts)) + (var-set coinbaseAmount2 (get coinbaseAmount2 coinbaseAmounts)) + (var-set coinbaseAmount3 (get coinbaseAmount3 coinbaseAmounts)) + (var-set coinbaseAmount4 (get coinbaseAmount4 coinbaseAmounts)) + (var-set coinbaseAmount5 (get coinbaseAmount5 coinbaseAmounts)) + (var-set coinbaseAmountDefault (get coinbaseAmountDefault coinbaseAmounts)) + ;; print coinbase amounts + (print { + coinbaseAmountBonus: (var-get coinbaseAmountBonus), + coinbaseAmount1: (var-get coinbaseAmount1), + coinbaseAmount2: (var-get coinbaseAmount2), + coinbaseAmount3: (var-get coinbaseAmount3), + coinbaseAmount4: (var-get coinbaseAmount4), + coinbaseAmount5: (var-get coinbaseAmount5), + coinbaseAmountDefault: (var-get coinbaseAmountDefault) + }) + (ok true) + ) +) + +;; guarded function for auth to update coinbase amounts +(define-public (update-coinbase-amounts) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (try! (set-coinbase-amounts)) + (ok true) + ) +) + +;; function for deciding how many tokens to mint, depending on when they were mined +(define-read-only (get-coinbase-amount (minerBlockHeight uint)) + (begin + ;; if contract is not active, return 0 + (asserts! (>= minerBlockHeight (var-get activationBlock)) u0) + ;; if contract is active, return based on emissions schedule + ;; defined in CCIP-008 https://github.com/citycoins/governance + (asserts! (> minerBlockHeight (var-get coinbaseThreshold1)) + (if (<= (- minerBlockHeight (var-get activationBlock)) TOKEN_BONUS_PERIOD) + ;; bonus reward for initial miners + (var-get coinbaseAmountBonus) + ;; standard reward until 1st halving + (var-get coinbaseAmount1) + ) + ) + ;; computations based on each halving threshold + (asserts! (> minerBlockHeight (var-get coinbaseThreshold2)) (var-get coinbaseAmount2)) + (asserts! (> minerBlockHeight (var-get coinbaseThreshold3)) (var-get coinbaseAmount3)) + (asserts! (> minerBlockHeight (var-get coinbaseThreshold4)) (var-get coinbaseAmount4)) + (asserts! (> minerBlockHeight (var-get coinbaseThreshold5)) (var-get coinbaseAmount5)) + ;; default value after 5th halving + (var-get coinbaseAmountDefault) + ) +) + +;; mint new tokens for claimant who won at given Stacks block height +(define-private (mint-coinbase (recipient principal) (stacksHeight uint)) + (as-contract (contract-call? 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 mint (get-coinbase-amount stacksHeight) recipient)) +) + +;; UTILITIES + +(define-data-var shutdownHeight uint u0) +(define-data-var isShutdown bool false) + +;; stop mining and stacking operations +;; in preparation for a core upgrade +(define-public (shutdown-contract (stacksHeight uint)) + (begin + ;; make sure block height is in the future + (asserts! (>= stacksHeight block-height) ERR_BLOCK_HEIGHT_IN_PAST) + ;; only allow shutdown request from AUTH + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + ;; set variables to disable mining/stacking in CORE + (var-set activationReached false) + (var-set shutdownHeight stacksHeight) + ;; set variable to allow for all stacking claims + (var-set isShutdown true) + (ok true) + ) +) + +;; checks if caller is Auth contract +(define-private (is-authorized-auth) + (is-eq contract-caller 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-auth-v2) +) + +;; checks if contract is fully activated to +;; enable mining and stacking functions +(define-private (is-activated) + (and (get-activation-status) (>= block-height (var-get activationTarget))) +) diff --git a/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-core-v2.json b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-core-v2.json new file mode 100644 index 00000000..89c8a70a --- /dev/null +++ b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-core-v2.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2.clar b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2.clar new file mode 100644 index 00000000..63a6f606 --- /dev/null +++ b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2.clar @@ -0,0 +1,301 @@ +;; NEWYORKCITYCOIN TOKEN V2 CONTRACT +;; CityCoins Protocol Version 2.0.0 + +;; TRAIT DEFINITIONS + +(impl-trait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-token-trait.citycoin-token) +(impl-trait 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-token-v2-trait.citycoin-token-v2) +(use-trait coreTrait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.citycoin-core) + +;; ERROR CODES + +(define-constant ERR_UNAUTHORIZED (err u2000)) +(define-constant ERR_TOKEN_NOT_ACTIVATED (err u2001)) +(define-constant ERR_TOKEN_ALREADY_ACTIVATED (err u2002)) +(define-constant ERR_V1_BALANCE_NOT_FOUND (err u2003)) +(define-constant ERR_INVALID_COINBASE_THRESHOLD (err u2004)) +(define-constant ERR_INVALID_COINBASE_AMOUNT (err u2005)) + +;; SIP-010 DEFINITION + +(impl-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait) + +(define-fungible-token newyorkcitycoin) + +(define-constant DECIMALS u6) +(define-constant MICRO_CITYCOINS (pow u10 DECIMALS)) + +;; SIP-010 FUNCTIONS + +(define-public (transfer (amount uint) (from principal) (to principal) (memo (optional (buff 34)))) + (begin + (asserts! (is-eq from tx-sender) ERR_UNAUTHORIZED) + (if (is-some memo) + (print memo) + none + ) + (ft-transfer? newyorkcitycoin amount from to) + ) +) + +(define-read-only (get-name) + (ok "newyorkcitycoin") +) + +(define-read-only (get-symbol) + (ok "NYC") +) + +(define-read-only (get-decimals) + (ok DECIMALS) +) + +(define-read-only (get-balance (user principal)) + (ok (ft-get-balance newyorkcitycoin user)) +) + +(define-read-only (get-total-supply) + (ok (ft-get-supply newyorkcitycoin)) +) + +(define-read-only (get-token-uri) + (ok (var-get tokenUri)) +) + +;; TOKEN CONFIGURATION + +;; define bonus period and initial epoch length +(define-constant TOKEN_BONUS_PERIOD u10000) +(define-constant TOKEN_EPOCH_LENGTH u25000) + +;; once activated, activation cannot happen again +(define-data-var tokenActivated bool false) + +;; core contract states +(define-constant STATE_DEPLOYED u0) +(define-constant STATE_ACTIVE u1) +(define-constant STATE_INACTIVE u2) + +;; one-time function to activate the token +(define-public (activate-token (coreContract principal) (stacksHeight uint)) + (let + ( + (coreContractMap (try! (contract-call? 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-auth-v2 get-core-contract-info coreContract))) + (threshold1 (+ stacksHeight TOKEN_BONUS_PERIOD TOKEN_EPOCH_LENGTH)) ;; 35,000 blocks + (threshold2 (+ stacksHeight TOKEN_BONUS_PERIOD (* u3 TOKEN_EPOCH_LENGTH))) ;; 85,000 blocks + (threshold3 (+ stacksHeight TOKEN_BONUS_PERIOD (* u7 TOKEN_EPOCH_LENGTH))) ;; 185,000 blocks + (threshold4 (+ stacksHeight TOKEN_BONUS_PERIOD (* u15 TOKEN_EPOCH_LENGTH))) ;; 385,000 blocks + (threshold5 (+ stacksHeight TOKEN_BONUS_PERIOD (* u31 TOKEN_EPOCH_LENGTH))) ;; 785,000 blocks + ) + (asserts! (is-eq (get state coreContractMap) STATE_ACTIVE) ERR_UNAUTHORIZED) + (asserts! (not (var-get tokenActivated)) ERR_TOKEN_ALREADY_ACTIVATED) + (var-set tokenActivated true) + (try! (set-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5)) + (ok true) + ) +) + +;; COINBASE THRESHOLDS + +;; coinbase thresholds per halving, used to select coinbase rewards in core +;; initially set by register-user in core contract per CCIP-008 +(define-data-var coinbaseThreshold1 uint u0) +(define-data-var coinbaseThreshold2 uint u0) +(define-data-var coinbaseThreshold3 uint u0) +(define-data-var coinbaseThreshold4 uint u0) +(define-data-var coinbaseThreshold5 uint u0) + +;; return coinbase thresholds if token activated +(define-read-only (get-coinbase-thresholds) + (let + ( + (activated (var-get tokenActivated)) + ) + (asserts! activated ERR_TOKEN_NOT_ACTIVATED) + (ok { + coinbaseThreshold1: (var-get coinbaseThreshold1), + coinbaseThreshold2: (var-get coinbaseThreshold2), + coinbaseThreshold3: (var-get coinbaseThreshold3), + coinbaseThreshold4: (var-get coinbaseThreshold4), + coinbaseThreshold5: (var-get coinbaseThreshold5) + }) + ) +) + +(define-private (set-coinbase-thresholds (threshold1 uint) (threshold2 uint) (threshold3 uint) (threshold4 uint) (threshold5 uint)) + (begin + ;; check that all thresholds increase in value + (asserts! (and (> threshold1 u0) (> threshold2 threshold1) (> threshold3 threshold2) (> threshold4 threshold3) (> threshold5 threshold4)) ERR_INVALID_COINBASE_THRESHOLD) + ;; set coinbase thresholds + (var-set coinbaseThreshold1 threshold1) + (var-set coinbaseThreshold2 threshold2) + (var-set coinbaseThreshold3 threshold3) + (var-set coinbaseThreshold4 threshold4) + (var-set coinbaseThreshold5 threshold5) + ;; print coinbase thresholds + (print { + coinbaseThreshold1: threshold1, + coinbaseThreshold2: threshold2, + coinbaseThreshold3: threshold3, + coinbaseThreshold4: threshold4, + coinbaseThreshold5: threshold5 + }) + (ok true) + ) +) + +;; only accessible by auth +(define-public (update-coinbase-thresholds (threshold1 uint) (threshold2 uint) (threshold3 uint) (threshold4 uint) (threshold5 uint)) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (asserts! (var-get tokenActivated) ERR_TOKEN_NOT_ACTIVATED) + (try! (set-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5)) + (ok true) + ) +) + +;; COINBASE AMOUNTS (REWARDS) + +;; coinbase rewards per threshold per CCIP-008 +(define-data-var coinbaseAmountBonus uint (* MICRO_CITYCOINS u250000)) +(define-data-var coinbaseAmount1 uint (* MICRO_CITYCOINS u100000)) +(define-data-var coinbaseAmount2 uint (* MICRO_CITYCOINS u50000)) +(define-data-var coinbaseAmount3 uint (* MICRO_CITYCOINS u25000)) +(define-data-var coinbaseAmount4 uint (* MICRO_CITYCOINS u12500)) +(define-data-var coinbaseAmount5 uint (* MICRO_CITYCOINS u6250)) +(define-data-var coinbaseAmountDefault uint (* MICRO_CITYCOINS u3125)) + +;; return coinbase thresholds if token activated +(define-read-only (get-coinbase-amounts) + (ok { + coinbaseAmountBonus: (var-get coinbaseAmountBonus), + coinbaseAmount1: (var-get coinbaseAmount1), + coinbaseAmount2: (var-get coinbaseAmount2), + coinbaseAmount3: (var-get coinbaseAmount3), + coinbaseAmount4: (var-get coinbaseAmount4), + coinbaseAmount5: (var-get coinbaseAmount5), + coinbaseAmountDefault: (var-get coinbaseAmountDefault) + }) +) + +(define-private (set-coinbase-amounts (amountBonus uint) (amount1 uint) (amount2 uint) (amount3 uint) (amount4 uint) (amount5 uint) (amountDefault uint)) + (begin + ;; check that all amounts are greater than zero + (asserts! (and (> amountBonus u0) (> amount1 u0) (> amount2 u0) (> amount3 u0) (> amount4 u0) (> amount5 u0) (> amountDefault u0)) ERR_INVALID_COINBASE_AMOUNT) + ;; set coinbase amounts in token contract + (var-set coinbaseAmountBonus amountBonus) + (var-set coinbaseAmount1 amount1) + (var-set coinbaseAmount2 amount2) + (var-set coinbaseAmount3 amount3) + (var-set coinbaseAmount4 amount4) + (var-set coinbaseAmount5 amount5) + (var-set coinbaseAmountDefault amountDefault) + ;; print coinbase amounts + (print { + coinbaseAmountBonus: amountBonus, + coinbaseAmount1: amount1, + coinbaseAmount2: amount2, + coinbaseAmount3: amount3, + coinbaseAmount4: amount4, + coinbaseAmount5: amount5, + coinbaseAmountDefault: amountDefault + }) + (ok true) + ) +) + +;; only accessible by auth +(define-public (update-coinbase-amounts (amountBonus uint) (amount1 uint) (amount2 uint) (amount3 uint) (amount4 uint) (amount5 uint) (amountDefault uint)) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + ;; (asserts! (var-get tokenActivated) ERR_TOKEN_NOT_ACTIVATED) + (try! (set-coinbase-amounts amountBonus amount1 amount2 amount3 amount4 amount5 amountDefault)) + (ok true) + ) +) + +;; V1 TO V2 CONVERSION + +(define-public (convert-to-v2) + (let + ( + (balanceV1 (unwrap! (contract-call? 'SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token get-balance tx-sender) ERR_V1_BALANCE_NOT_FOUND)) + ) + ;; verify positive balance + (asserts! (> balanceV1 u0) ERR_V1_BALANCE_NOT_FOUND) + ;; burn old + (try! (contract-call? 'SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token burn balanceV1 tx-sender)) + (print { + burnedV1: balanceV1, + mintedV2: (* balanceV1 MICRO_CITYCOINS), + tx-sender: tx-sender, + contract-caller: contract-caller + }) + ;; create new + (ft-mint? newyorkcitycoin (* balanceV1 MICRO_CITYCOINS) tx-sender) + ) +) + +;; UTILITIES + +(define-data-var tokenUri (optional (string-utf8 256)) (some u"https://cdn.citycoins.co/metadata/newyorkcitycoin.json")) + +;; set token URI to new value, only accessible by Auth +(define-public (set-token-uri (newUri (optional (string-utf8 256)))) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (ok (var-set tokenUri newUri)) + ) +) + +;; mint new tokens, only accessible by a Core contract +(define-public (mint (amount uint) (recipient principal)) + (let + ( + (coreContract (try! (contract-call? 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-auth-v2 get-core-contract-info contract-caller))) + ) + (ft-mint? newyorkcitycoin amount recipient) + ) +) + +;; burn tokens +(define-public (burn (amount uint) (owner principal)) + (begin + (asserts! (is-eq tx-sender owner) ERR_UNAUTHORIZED) + (ft-burn? newyorkcitycoin amount owner) + ) +) + +;; checks if caller is Auth contract +(define-private (is-authorized-auth) + (is-eq contract-caller 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-auth-v2) +) + +;; SEND-MANY + +(define-public (send-many (recipients (list 200 { to: principal, amount: uint, memo: (optional (buff 34)) }))) + (fold check-err + (map send-token recipients) + (ok true) + ) +) + +(define-private (check-err (result (response bool uint)) (prior (response bool uint))) + (match prior ok-value + result + err-value (err err-value) + ) +) + +(define-private (send-token (recipient { to: principal, amount: uint, memo: (optional (buff 34)) })) + (send-token-with-memo (get amount recipient) (get to recipient) (get memo recipient)) +) + +(define-private (send-token-with-memo (amount uint) (to principal) (memo (optional (buff 34)))) + (let + ( + (transferOk (try! (transfer amount tx-sender to memo))) + ) + (ok transferOk) + ) +) diff --git a/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2.json b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2.json new file mode 100644 index 00000000..89c8a70a --- /dev/null +++ b/.cache/requirements/SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/ST000000000000000000002AMW42H.pox.clar b/.cache/requirements/ST000000000000000000002AMW42H.pox.clar new file mode 100644 index 00000000..ac1c532a --- /dev/null +++ b/.cache/requirements/ST000000000000000000002AMW42H.pox.clar @@ -0,0 +1,695 @@ +;; PoX testnet constants +;; Min/max number of reward cycles uSTX can be locked for +(define-constant MIN_POX_REWARD_CYCLES u1) +(define-constant MAX_POX_REWARD_CYCLES u12) + +;; Default length of the PoX registration window, in burnchain blocks. +(define-constant PREPARE_CYCLE_LENGTH u50) + +;; Default length of the PoX reward cycle, in burnchain blocks. +(define-constant REWARD_CYCLE_LENGTH u1050) + +;; Valid values for burnchain address versions. +;; These correspond to address hash modes in Stacks 2.0. +(define-constant ADDRESS_VERSION_P2PKH 0x00) +(define-constant ADDRESS_VERSION_P2SH 0x01) +(define-constant ADDRESS_VERSION_P2WPKH 0x02) +(define-constant ADDRESS_VERSION_P2WSH 0x03) + +;; Stacking thresholds +(define-constant STACKING_THRESHOLD_25 u8000) +(define-constant STACKING_THRESHOLD_100 u2000) + +;; The .pox contract +;; Error codes +(define-constant ERR_STACKING_UNREACHABLE 255) +(define-constant ERR_STACKING_INSUFFICIENT_FUNDS 1) +(define-constant ERR_STACKING_INVALID_LOCK_PERIOD 2) +(define-constant ERR_STACKING_ALREADY_STACKED 3) +(define-constant ERR_STACKING_NO_SUCH_PRINCIPAL 4) +(define-constant ERR_STACKING_EXPIRED 5) +(define-constant ERR_STACKING_STX_LOCKED 6) +(define-constant ERR_STACKING_PERMISSION_DENIED 9) +(define-constant ERR_STACKING_THRESHOLD_NOT_MET 11) +(define-constant ERR_STACKING_POX_ADDRESS_IN_USE 12) +(define-constant ERR_STACKING_INVALID_POX_ADDRESS 13) +(define-constant ERR_STACKING_ALREADY_REJECTED 17) +(define-constant ERR_STACKING_INVALID_AMOUNT 18) +(define-constant ERR_NOT_ALLOWED 19) +(define-constant ERR_STACKING_ALREADY_DELEGATED 20) +(define-constant ERR_DELEGATION_EXPIRES_DURING_LOCK 21) +(define-constant ERR_DELEGATION_TOO_MUCH_LOCKED 22) +(define-constant ERR_DELEGATION_POX_ADDR_REQUIRED 23) +(define-constant ERR_INVALID_START_BURN_HEIGHT 24) + +;; PoX disabling threshold (a percent) +(define-constant POX_REJECTION_FRACTION u25) + +;; Data vars that store a copy of the burnchain configuration. +;; Implemented as data-vars, so that different configurations can be +;; used in e.g. test harnesses. +(define-data-var pox-prepare-cycle-length uint PREPARE_CYCLE_LENGTH) +(define-data-var pox-reward-cycle-length uint REWARD_CYCLE_LENGTH) +(define-data-var pox-rejection-fraction uint POX_REJECTION_FRACTION) +(define-data-var first-burnchain-block-height uint u0) +(define-data-var configured bool false) + +;; This function can only be called once, when it boots up +(define-public (set-burnchain-parameters (first-burn-height uint) (prepare-cycle-length uint) (reward-cycle-length uint) (rejection-fraction uint)) + (begin + (asserts! (not (var-get configured)) (err ERR_NOT_ALLOWED)) + (var-set first-burnchain-block-height first-burn-height) + (var-set pox-prepare-cycle-length prepare-cycle-length) + (var-set pox-reward-cycle-length reward-cycle-length) + (var-set pox-rejection-fraction rejection-fraction) + (var-set configured true) + (ok true)) +) + +;; The Stacking lock-up state and associated metadata. +;; Records can be inserted into this map via one of two ways: +;; * via contract-call? to the (stack-stx) method, or +;; * via a transaction in the underlying burnchain that encodes the same data. +;; In the latter case, this map will be updated by the Stacks +;; node itself, and transactions in the burnchain will take priority +;; over transactions in the Stacks chain when processing this block. +(define-map stacking-state + { stacker: principal } + { + ;; how many uSTX locked? + amount-ustx: uint, + ;; Description of the underlying burnchain address that will + ;; receive PoX'ed tokens. Translating this into an address + ;; depends on the burnchain being used. When Bitcoin is + ;; the burnchain, this gets translated into a p2pkh, p2sh, + ;; p2wpkh-p2sh, or p2wsh-p2sh UTXO, depending on the version. + pox-addr: { version: (buff 1), hashbytes: (buff 20) }, + ;; how long the uSTX are locked, in reward cycles. + lock-period: uint, + ;; reward cycle when rewards begin + first-reward-cycle: uint + } +) + +;; Delegation relationships +(define-map delegation-state + { stacker: principal } + { + amount-ustx: uint, ;; how many uSTX delegated? + delegated-to: principal, ;; who are we delegating? + until-burn-ht: (optional uint), ;; how long does the delegation last? + ;; does the delegate _need_ to use a specific + ;; pox recipient address? + pox-addr: (optional { version: (buff 1), hashbytes: (buff 20) }) + } +) + +;; allowed contract-callers +(define-map allowance-contract-callers + { sender: principal, contract-caller: principal } + { until-burn-ht: (optional uint) }) + +;; How many uSTX are stacked in a given reward cycle. +;; Updated when a new PoX address is registered, or when more STX are granted +;; to it. +(define-map reward-cycle-total-stacked + { reward-cycle: uint } + { total-ustx: uint } +) + +;; Internal map read by the Stacks node to iterate through the list of +;; PoX reward addresses on a per-reward-cycle basis. +(define-map reward-cycle-pox-address-list + { reward-cycle: uint, index: uint } + { + pox-addr: { version: (buff 1), hashbytes: (buff 20) }, + total-ustx: uint + } +) + +(define-map reward-cycle-pox-address-list-len + { reward-cycle: uint } + { len: uint } +) + +;; how much has been locked up for this address before +;; committing? +;; this map allows stackers to stack amounts < minimum +;; by paying the cost of aggregation during the commit +(define-map partial-stacked-by-cycle + { + pox-addr: { version: (buff 1), hashbytes: (buff 20) }, + reward-cycle: uint, + sender: principal + } + { stacked-amount: uint } +) + +;; Amount of uSTX that reject PoX, by reward cycle +(define-map stacking-rejection + { reward-cycle: uint } + { amount: uint } +) + +;; Who rejected in which reward cycle +(define-map stacking-rejectors + { stacker: principal, reward-cycle: uint } + { amount: uint } +) + +;; Getter for stacking-rejectors +(define-read-only (get-pox-rejection (stacker principal) (reward-cycle uint)) + (map-get? stacking-rejectors { stacker: stacker, reward-cycle: reward-cycle })) + +;; Has PoX been rejected in the given reward cycle? +(define-read-only (is-pox-active (reward-cycle uint)) + (let ( + (reject-votes + (default-to + u0 + (get amount (map-get? stacking-rejection { reward-cycle: reward-cycle })))) + ) + ;; (100 * reject-votes) / stx-liquid-supply < pox-rejection-fraction + (< (* u100 reject-votes) + (* (var-get pox-rejection-fraction) stx-liquid-supply))) +) + +;; What's the reward cycle number of the burnchain block height? +;; Will runtime-abort if height is less than the first burnchain block (this is intentional) +(define-private (burn-height-to-reward-cycle (height uint)) + (/ (- height (var-get first-burnchain-block-height)) (var-get pox-reward-cycle-length))) + +;; What's the block height at the start of a given reward cycle? +(define-private (reward-cycle-to-burn-height (cycle uint)) + (+ (var-get first-burnchain-block-height) (* cycle (var-get pox-reward-cycle-length)))) + +;; What's the current PoX reward cycle? +(define-private (current-pox-reward-cycle) + (burn-height-to-reward-cycle burn-block-height)) + +;; Get the _current_ PoX stacking principal information. If the information +;; is expired, or if there's never been such a stacker, then returns none. +(define-read-only (get-stacker-info (stacker principal)) + (match (map-get? stacking-state { stacker: stacker }) + stacking-info + (if (<= (+ (get first-reward-cycle stacking-info) (get lock-period stacking-info)) (current-pox-reward-cycle)) + ;; present, but lock has expired + none + ;; present, and lock has not expired + (some stacking-info) + ) + ;; no state at all + none + )) + +(define-private (check-caller-allowed) + (or (is-eq tx-sender contract-caller) + (let ((caller-allowed + ;; if not in the caller map, return false + (unwrap! (map-get? allowance-contract-callers + { sender: tx-sender, contract-caller: contract-caller }) + false))) + ;; is the caller allowance expired? + (if (< burn-block-height (unwrap! (get until-burn-ht caller-allowed) true)) + false + true)))) + +(define-private (get-check-delegation (stacker principal)) + (let ((delegation-info (try! (map-get? delegation-state { stacker: stacker })))) + ;; did the existing delegation expire? + (if (match (get until-burn-ht delegation-info) + until-burn-ht (> burn-block-height until-burn-ht) + false) + ;; it expired, return none + none + ;; delegation is active + (some delegation-info)))) + +;; Get the size of the reward set for a reward cycle. +;; Note that this does _not_ return duplicate PoX addresses. +;; Note that this also _will_ return PoX addresses that are beneath +;; the minimum threshold -- i.e. the threshold can increase after insertion. +;; Used internally by the Stacks node, which filters out the entries +;; in this map to select PoX addresses with enough STX. +(define-read-only (get-reward-set-size (reward-cycle uint)) + (default-to + u0 + (get len (map-get? reward-cycle-pox-address-list-len { reward-cycle: reward-cycle })))) + +;; How many rejection votes have we been accumulating for the next block +(define-private (next-cycle-rejection-votes) + (default-to + u0 + (get amount (map-get? stacking-rejection { reward-cycle: (+ u1 (current-pox-reward-cycle)) })))) + +;; Add a single PoX address to a single reward cycle. +;; Used to build up a set of per-reward-cycle PoX addresses. +;; No checking will be done -- don't call if this PoX address is already registered in this reward cycle! +(define-private (append-reward-cycle-pox-addr (pox-addr (tuple (version (buff 1)) (hashbytes (buff 20)))) + (reward-cycle uint) + (amount-ustx uint)) + (let ( + (sz (get-reward-set-size reward-cycle)) + ) + (map-set reward-cycle-pox-address-list + { reward-cycle: reward-cycle, index: sz } + { pox-addr: pox-addr, total-ustx: amount-ustx }) + (map-set reward-cycle-pox-address-list-len + { reward-cycle: reward-cycle } + { len: (+ u1 sz) }) + (+ u1 sz)) +) + +;; How many uSTX are stacked? +(define-read-only (get-total-ustx-stacked (reward-cycle uint)) + (default-to + u0 + (get total-ustx (map-get? reward-cycle-total-stacked { reward-cycle: reward-cycle }))) +) + +;; Called internally by the node to iterate through the list of PoX addresses in this reward cycle. +;; Returns (optional (tuple (pox-addr ) (total-ustx ))) +(define-read-only (get-reward-set-pox-address (reward-cycle uint) (index uint)) + (map-get? reward-cycle-pox-address-list { reward-cycle: reward-cycle, index: index })) + +;; Add a PoX address to the ith reward cycle, if i is between 0 and the given num-cycles (exclusive). +;; Arguments are given as a tuple, so this function can be (map ..)'ed onto a list of its arguments. +;; Used by add-pox-addr-to-reward-cycles. +;; No checking is done. +;; Returns 1 if added. +;; Returns 0 if not added. +(define-private (add-pox-addr-to-ith-reward-cycle (cycle-index uint) (params (tuple + (pox-addr (tuple (version (buff 1)) (hashbytes (buff 20)))) + (first-reward-cycle uint) + (num-cycles uint) + (amount-ustx uint) + (i uint)))) + (let ((reward-cycle (+ (get first-reward-cycle params) (get i params))) + (num-cycles (get num-cycles params)) + (i (get i params))) + { + pox-addr: (get pox-addr params), + first-reward-cycle: (get first-reward-cycle params), + num-cycles: num-cycles, + amount-ustx: (get amount-ustx params), + i: (if (< i num-cycles) + (let ((total-ustx (get-total-ustx-stacked reward-cycle))) + ;; record how many uSTX this pox-addr will stack for in the given reward cycle + (append-reward-cycle-pox-addr + (get pox-addr params) + reward-cycle + (get amount-ustx params)) + + ;; update running total + (map-set reward-cycle-total-stacked + { reward-cycle: reward-cycle } + { total-ustx: (+ (get amount-ustx params) total-ustx) }) + + ;; updated _this_ reward cycle + (+ i u1)) + (+ i u0)) + })) + +;; Add a PoX address to a given sequence of reward cycle lists. +;; A PoX address can be added to at most 12 consecutive cycles. +;; No checking is done. +(define-private (add-pox-addr-to-reward-cycles (pox-addr (tuple (version (buff 1)) (hashbytes (buff 20)))) + (first-reward-cycle uint) + (num-cycles uint) + (amount-ustx uint)) + (let ((cycle-indexes (list u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11))) + ;; For safety, add up the number of times (add-principal-to-ith-reward-cycle) returns 1. + ;; It _should_ be equal to num-cycles. + (asserts! + (is-eq num-cycles + (get i (fold add-pox-addr-to-ith-reward-cycle cycle-indexes + { pox-addr: pox-addr, first-reward-cycle: first-reward-cycle, num-cycles: num-cycles, amount-ustx: amount-ustx, i: u0 }))) + (err ERR_STACKING_UNREACHABLE)) + (ok true))) + +(define-private (add-pox-partial-stacked-to-ith-cycle + (cycle-index uint) + (params { pox-addr: { version: (buff 1), hashbytes: (buff 20) }, + reward-cycle: uint, + num-cycles: uint, + amount-ustx: uint })) + (let ((pox-addr (get pox-addr params)) + (num-cycles (get num-cycles params)) + (reward-cycle (get reward-cycle params)) + (amount-ustx (get amount-ustx params))) + (let ((current-amount + (default-to u0 + (get stacked-amount + (map-get? partial-stacked-by-cycle { sender: tx-sender, pox-addr: pox-addr, reward-cycle: reward-cycle }))))) + (if (>= cycle-index num-cycles) + ;; do not add to cycles >= cycle-index + false + ;; otherwise, add to the partial-stacked-by-cycle + (map-set partial-stacked-by-cycle + { sender: tx-sender, pox-addr: pox-addr, reward-cycle: reward-cycle } + { stacked-amount: (+ amount-ustx current-amount) })) + ;; produce the next params tuple + { pox-addr: pox-addr, + reward-cycle: (+ u1 reward-cycle), + num-cycles: num-cycles, + amount-ustx: amount-ustx }))) + +;; Add a PoX address to a given sequence of partial reward cycle lists. +;; A PoX address can be added to at most 12 consecutive cycles. +;; No checking is done. +(define-private (add-pox-partial-stacked (pox-addr (tuple (version (buff 1)) (hashbytes (buff 20)))) + (first-reward-cycle uint) + (num-cycles uint) + (amount-ustx uint)) + (let ((cycle-indexes (list u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11))) + (fold add-pox-partial-stacked-to-ith-cycle cycle-indexes + { pox-addr: pox-addr, reward-cycle: first-reward-cycle, num-cycles: num-cycles, amount-ustx: amount-ustx }) + true)) + +;; What is the minimum number of uSTX to be stacked in the given reward cycle? +;; Used internally by the Stacks node, and visible publicly. +(define-read-only (get-stacking-minimum) + (/ stx-liquid-supply STACKING_THRESHOLD_25)) + +;; Is the address mode valid for a PoX burn address? +(define-private (check-pox-addr-version (version (buff 1))) + (or (is-eq version ADDRESS_VERSION_P2PKH) + (is-eq version ADDRESS_VERSION_P2SH) + (is-eq version ADDRESS_VERSION_P2WPKH) + (is-eq version ADDRESS_VERSION_P2WSH))) + +;; Is the given lock period valid? +(define-private (check-pox-lock-period (lock-period uint)) + (and (>= lock-period MIN_POX_REWARD_CYCLES) + (<= lock-period MAX_POX_REWARD_CYCLES))) + +;; Evaluate if a participant can stack an amount of STX for a given period. +;; This method is designed as a read-only method so that it can be used as +;; a set of guard conditions and also as a read-only RPC call that can be +;; performed beforehand. +(define-read-only (can-stack-stx (pox-addr (tuple (version (buff 1)) (hashbytes (buff 20)))) + (amount-ustx uint) + (first-reward-cycle uint) + (num-cycles uint)) + (begin + ;; minimum uSTX must be met + (asserts! (<= (print (get-stacking-minimum)) amount-ustx) + (err ERR_STACKING_THRESHOLD_NOT_MET)) + + (minimal-can-stack-stx pox-addr amount-ustx first-reward-cycle num-cycles))) + +;; Evaluate if a participant can stack an amount of STX for a given period. +;; This method is designed as a read-only method so that it can be used as +;; a set of guard conditions and also as a read-only RPC call that can be +;; performed beforehand. +(define-read-only (minimal-can-stack-stx + (pox-addr (tuple (version (buff 1)) (hashbytes (buff 20)))) + (amount-ustx uint) + (first-reward-cycle uint) + (num-cycles uint)) + (begin + ;; amount must be valid + (asserts! (> amount-ustx u0) + (err ERR_STACKING_INVALID_AMOUNT)) + + ;; sender principal must not have rejected in this upcoming reward cycle + (asserts! (is-none (get-pox-rejection tx-sender first-reward-cycle)) + (err ERR_STACKING_ALREADY_REJECTED)) + + ;; lock period must be in acceptable range. + (asserts! (check-pox-lock-period num-cycles) + (err ERR_STACKING_INVALID_LOCK_PERIOD)) + + ;; address version must be valid + (asserts! (check-pox-addr-version (get version pox-addr)) + (err ERR_STACKING_INVALID_POX_ADDRESS)) + (ok true))) + +;; Revoke contract-caller authorization to call stacking methods +(define-public (disallow-contract-caller (caller principal)) + (begin + (asserts! (is-eq tx-sender contract-caller) + (err ERR_STACKING_PERMISSION_DENIED)) + (ok (map-delete allowance-contract-callers { sender: tx-sender, contract-caller: caller })))) + +;; Give a contract-caller authorization to call stacking methods +;; normally, stacking methods may only be invoked by _direct_ transactions +;; (i.e., the tx-sender issues a direct contract-call to the stacking methods) +;; by issuing an allowance, the tx-sender may call through the allowed contract +(define-public (allow-contract-caller (caller principal) (until-burn-ht (optional uint))) + (begin + (asserts! (is-eq tx-sender contract-caller) + (err ERR_STACKING_PERMISSION_DENIED)) + (ok (map-set allowance-contract-callers + { sender: tx-sender, contract-caller: caller } + { until-burn-ht: until-burn-ht })))) + +;; Lock up some uSTX for stacking! Note that the given amount here is in micro-STX (uSTX). +;; The STX will be locked for the given number of reward cycles (lock-period). +;; This is the self-service interface. tx-sender will be the Stacker. +;; +;; * The given stacker cannot currently be stacking. +;; * You will need the minimum uSTX threshold. This will be determined by (get-stacking-minimum) +;; at the time this method is called. +;; * You may need to increase the amount of uSTX locked up later, since the minimum uSTX threshold +;; may increase between reward cycles. +;; * The Stacker will receive rewards in the reward cycle following `start-burn-ht`. +;; Importantly, `start-burn-ht` may not be further into the future than the next reward cycle, +;; and in most cases should be set to the current burn block height. +;; +;; The tokens will unlock and be returned to the Stacker (tx-sender) automatically. +(define-public (stack-stx (amount-ustx uint) + (pox-addr (tuple (version (buff 1)) (hashbytes (buff 20)))) + (start-burn-ht uint) + (lock-period uint)) + ;; this stacker's first reward cycle is the _next_ reward cycle + (let ((first-reward-cycle (+ u1 (current-pox-reward-cycle))) + (specified-reward-cycle (+ u1 (burn-height-to-reward-cycle start-burn-ht)))) + ;; the start-burn-ht must result in the next reward cycle, do not allow stackers + ;; to "post-date" their `stack-stx` transaction + (asserts! (is-eq first-reward-cycle specified-reward-cycle) + (err ERR_INVALID_START_BURN_HEIGHT)) + + ;; must be called directly by the tx-sender or by an allowed contract-caller + (asserts! (check-caller-allowed) + (err ERR_STACKING_PERMISSION_DENIED)) + + ;; tx-sender principal must not be stacking + (asserts! (is-none (get-stacker-info tx-sender)) + (err ERR_STACKING_ALREADY_STACKED)) + + ;; tx-sender must not be delegating + (asserts! (is-none (get-check-delegation tx-sender)) + (err ERR_STACKING_ALREADY_DELEGATED)) + + ;; the Stacker must have sufficient unlocked funds + (asserts! (>= (stx-get-balance tx-sender) amount-ustx) + (err ERR_STACKING_INSUFFICIENT_FUNDS)) + + ;; ensure that stacking can be performed + (try! (can-stack-stx pox-addr amount-ustx first-reward-cycle lock-period)) + + ;; register the PoX address with the amount stacked + (try! (add-pox-addr-to-reward-cycles pox-addr first-reward-cycle lock-period amount-ustx)) + + ;; add stacker record + (map-set stacking-state + { stacker: tx-sender } + { amount-ustx: amount-ustx, + pox-addr: pox-addr, + first-reward-cycle: first-reward-cycle, + lock-period: lock-period }) + + ;; return the lock-up information, so the node can actually carry out the lock. + (ok { stacker: tx-sender, lock-amount: amount-ustx, unlock-burn-height: (reward-cycle-to-burn-height (+ first-reward-cycle lock-period)) })) +) + +(define-public (revoke-delegate-stx) + (begin + ;; must be called directly by the tx-sender or by an allowed contract-caller + (asserts! (check-caller-allowed) + (err ERR_STACKING_PERMISSION_DENIED)) + (ok (map-delete delegation-state { stacker: tx-sender })))) + +;; Delegate to `delegate-to` the ability to stack from a given address. +;; This method _does not_ lock the funds, rather, it allows the delegate +;; to issue the stacking lock. +;; The caller specifies: +;; * amount-ustx: the total amount of ustx the delegate may be allowed to lock +;; * until-burn-ht: an optional burn height at which this delegation expiration +;; * pox-addr: an optional address to which any rewards *must* be sent +(define-public (delegate-stx (amount-ustx uint) + (delegate-to principal) + (until-burn-ht (optional uint)) + (pox-addr (optional { version: (buff 1), + hashbytes: (buff 20) }))) + (begin + ;; must be called directly by the tx-sender or by an allowed contract-caller + (asserts! (check-caller-allowed) + (err ERR_STACKING_PERMISSION_DENIED)) + + ;; tx-sender principal must not be stacking + (asserts! (is-none (get-stacker-info tx-sender)) + (err ERR_STACKING_ALREADY_STACKED)) + + ;; tx-sender must not be delegating + (asserts! (is-none (get-check-delegation tx-sender)) + (err ERR_STACKING_ALREADY_DELEGATED)) + + ;; add delegation record + (map-set delegation-state + { stacker: tx-sender } + { amount-ustx: amount-ustx, + delegated-to: delegate-to, + until-burn-ht: until-burn-ht, + pox-addr: pox-addr }) + + (ok true))) + +;; Commit partially stacked STX. +;; This allows a stacker/delegate to lock fewer STX than the minimal threshold in multiple transactions, +;; so long as: 1. The pox-addr is the same. +;; 2. This "commit" transaction is called _before_ the PoX anchor block. +;; This ensures that each entry in the reward set returned to the stacks-node is greater than the threshold, +;; but does not require it be all locked up within a single transaction +(define-public (stack-aggregation-commit (pox-addr { version: (buff 1), hashbytes: (buff 20) }) + (reward-cycle uint)) + (let ((partial-stacked + ;; fetch the partial commitments + (unwrap! (map-get? partial-stacked-by-cycle { pox-addr: pox-addr, sender: tx-sender, reward-cycle: reward-cycle }) + (err ERR_STACKING_NO_SUCH_PRINCIPAL)))) + ;; must be called directly by the tx-sender or by an allowed contract-caller + (asserts! (check-caller-allowed) + (err ERR_STACKING_PERMISSION_DENIED)) + (let ((amount-ustx (get stacked-amount partial-stacked))) + (try! (can-stack-stx pox-addr amount-ustx reward-cycle u1)) + ;; add the pox addr to the reward cycle + (add-pox-addr-to-ith-reward-cycle + u0 + { pox-addr: pox-addr, + first-reward-cycle: reward-cycle, + num-cycles: u1, + amount-ustx: amount-ustx, + i: u0 }) + ;; don't update the stacking-state map, + ;; because it _already has_ this stacker's state + ;; don't lock the STX, because the STX is already locked + ;; + ;; clear the partial-stacked state + (map-delete partial-stacked-by-cycle { pox-addr: pox-addr, sender: tx-sender, reward-cycle: reward-cycle }) + (ok true)))) + +;; As a delegate, stack the given principal's STX using partial-stacked-by-cycle +;; Once the delegate has stacked > minimum, the delegate should call stack-aggregation-commit +(define-public (delegate-stack-stx (stacker principal) + (amount-ustx uint) + (pox-addr { version: (buff 1), hashbytes: (buff 20) }) + (start-burn-ht uint) + (lock-period uint)) + ;; this stacker's first reward cycle is the _next_ reward cycle + (let ((first-reward-cycle (+ u1 (current-pox-reward-cycle))) + (specified-reward-cycle (+ u1 (burn-height-to-reward-cycle start-burn-ht))) + (unlock-burn-height (reward-cycle-to-burn-height (+ (current-pox-reward-cycle) u1 lock-period)))) + ;; the start-burn-ht must result in the next reward cycle, do not allow stackers + ;; to "post-date" their `stack-stx` transaction + (asserts! (is-eq first-reward-cycle specified-reward-cycle) + (err ERR_INVALID_START_BURN_HEIGHT)) + + ;; must be called directly by the tx-sender or by an allowed contract-caller + (asserts! (check-caller-allowed) + (err ERR_STACKING_PERMISSION_DENIED)) + + ;; stacker must have delegated to the caller + (let ((delegation-info (unwrap! (get-check-delegation stacker) (err ERR_STACKING_PERMISSION_DENIED)))) + ;; must have delegated to tx-sender + (asserts! (is-eq (get delegated-to delegation-info) tx-sender) + (err ERR_STACKING_PERMISSION_DENIED)) + ;; must have delegated enough stx + (asserts! (>= (get amount-ustx delegation-info) amount-ustx) + (err ERR_DELEGATION_TOO_MUCH_LOCKED)) + ;; if pox-addr is set, must be equal to pox-addr + (asserts! (match (get pox-addr delegation-info) + specified-pox-addr (is-eq pox-addr specified-pox-addr) + true) + (err ERR_DELEGATION_POX_ADDR_REQUIRED)) + ;; delegation must not expire before lock period + (asserts! (match (get until-burn-ht delegation-info) + until-burn-ht (>= until-burn-ht + unlock-burn-height) + true) + (err ERR_DELEGATION_EXPIRES_DURING_LOCK))) + + ;; stacker principal must not be stacking + (asserts! (is-none (get-stacker-info stacker)) + (err ERR_STACKING_ALREADY_STACKED)) + + ;; the Stacker must have sufficient unlocked funds + (asserts! (>= (stx-get-balance stacker) amount-ustx) + (err ERR_STACKING_INSUFFICIENT_FUNDS)) + + ;; ensure that stacking can be performed + (try! (minimal-can-stack-stx pox-addr amount-ustx first-reward-cycle lock-period)) + + ;; register the PoX address with the amount stacked via partial stacking + ;; before it can be included in the reward set, this must be committed! + (add-pox-partial-stacked pox-addr first-reward-cycle lock-period amount-ustx) + + ;; add stacker record + (map-set stacking-state + { stacker: stacker } + { amount-ustx: amount-ustx, + pox-addr: pox-addr, + first-reward-cycle: first-reward-cycle, + lock-period: lock-period }) + + ;; return the lock-up information, so the node can actually carry out the lock. + (ok { stacker: stacker, + lock-amount: amount-ustx, + unlock-burn-height: unlock-burn-height }))) + +;; Reject Stacking for this reward cycle. +;; tx-sender votes all its uSTX for rejection. +;; Note that unlike PoX, rejecting PoX does not lock the tx-sender's +;; tokens. PoX rejection acts like a coin vote. +(define-public (reject-pox) + (let ( + (balance (stx-get-balance tx-sender)) + (vote-reward-cycle (+ u1 (current-pox-reward-cycle))) + ) + + ;; tx-sender principal must not have rejected in this upcoming reward cycle + (asserts! (is-none (get-pox-rejection tx-sender vote-reward-cycle)) + (err ERR_STACKING_ALREADY_REJECTED)) + + ;; tx-sender can't be a stacker + (asserts! (is-none (get-stacker-info tx-sender)) + (err ERR_STACKING_ALREADY_STACKED)) + + ;; vote for rejection + (map-set stacking-rejection + { reward-cycle: vote-reward-cycle } + { amount: (+ (next-cycle-rejection-votes) balance) } + ) + + ;; mark voted + (map-set stacking-rejectors + { stacker: tx-sender, reward-cycle: vote-reward-cycle } + { amount: balance } + ) + + (ok true)) +) + +;; Used for PoX parameters discovery +(define-read-only (get-pox-info) + (ok { + min-amount-ustx: (get-stacking-minimum), + reward-cycle-id: (current-pox-reward-cycle), + prepare-cycle-length: (var-get pox-prepare-cycle-length), + first-burnchain-block-height: (var-get first-burnchain-block-height), + reward-cycle-length: (var-get pox-reward-cycle-length), + rejection-fraction: (var-get pox-rejection-fraction), + current-rejection-votes: (next-cycle-rejection-votes), + total-liquid-supply-ustx: stx-liquid-supply, + }) +) diff --git a/.cache/requirements/ST000000000000000000002AMW42H.pox.json b/.cache/requirements/ST000000000000000000002AMW42H.pox.json new file mode 100644 index 00000000..11162a13 --- /dev/null +++ b/.cache/requirements/ST000000000000000000002AMW42H.pox.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch20", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-auth-v2.clar b/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-auth-v2.clar new file mode 100644 index 00000000..792c615d --- /dev/null +++ b/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-auth-v2.clar @@ -0,0 +1,630 @@ +;; MIAMICOIN AUTH CONTRACT V2 TESTNET +;; CityCoins Protocol Version 2.0.0 + +(define-constant CONTRACT_OWNER tx-sender) + +;; TRAIT DEFINITIONS + +(use-trait coreTraitV2 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-core-v2-trait.citycoin-core-v2) +(use-trait tokenTraitV2 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-token-v2-trait.citycoin-token-v2) + +;; ERRORS + +(define-constant ERR_UNKNOWN_JOB (err u6000)) +(define-constant ERR_UNAUTHORIZED (err u6001)) +(define-constant ERR_JOB_IS_ACTIVE (err u6002)) +(define-constant ERR_JOB_IS_NOT_ACTIVE (err u6003)) +(define-constant ERR_ALREADY_VOTED_THIS_WAY (err u6004)) +(define-constant ERR_JOB_IS_EXECUTED (err u6005)) +(define-constant ERR_JOB_IS_NOT_APPROVED (err u6006)) +(define-constant ERR_ARGUMENT_ALREADY_EXISTS (err u6007)) +(define-constant ERR_NO_ACTIVE_CORE_CONTRACT (err u6008)) +(define-constant ERR_CORE_CONTRACT_NOT_FOUND (err u6009)) +(define-constant ERR_UNKNOWN_ARGUMENT (err u6010)) +(define-constant ERR_INCORRECT_CONTRACT_STATE (err u6011)) +(define-constant ERR_CONTRACT_ALREADY_EXISTS (err u6012)) + +;; JOB MANAGEMENT + +(define-constant REQUIRED_APPROVALS u2) + +(define-data-var lastJobId uint u0) + +(define-map Jobs + uint + { + creator: principal, + name: (string-ascii 255), + target: principal, + approvals: uint, + disapprovals: uint, + isActive: bool, + isExecuted: bool + } +) + +(define-map JobApprovers + { jobId: uint, approver: principal } + bool +) + +(define-map Approvers + principal + bool +) + +(define-map ArgumentLastIdsByType + { jobId: uint, argumentType: (string-ascii 25) } + uint +) + +(define-map UIntArgumentsByName + { jobId: uint, argumentName: (string-ascii 255) } + { argumentId: uint, value: uint} +) + +(define-map UIntArgumentsById + { jobId: uint, argumentId: uint } + { argumentName: (string-ascii 255), value: uint } +) + +(define-map PrincipalArgumentsByName + { jobId: uint, argumentName: (string-ascii 255) } + { argumentId: uint, value: principal } +) + +(define-map PrincipalArgumentsById + { jobId: uint, argumentId: uint } + { argumentName: (string-ascii 255), value: principal } +) + +;; FUNCTIONS + +(define-read-only (get-last-job-id) + (var-get lastJobId) +) + +(define-public (create-job (name (string-ascii 255)) (target principal)) + (let + ( + (newJobId (+ (var-get lastJobId) u1)) + ) + (asserts! (is-approver tx-sender) ERR_UNAUTHORIZED) + (map-set Jobs + newJobId + { + creator: tx-sender, + name: name, + target: target, + approvals: u0, + disapprovals: u0, + isActive: false, + isExecuted: false + } + ) + (var-set lastJobId newJobId) + (ok newJobId) + ) +) + +(define-read-only (get-job (jobId uint)) + (map-get? Jobs jobId) +) + +(define-public (activate-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + ) + (asserts! (is-eq (get creator job) tx-sender) ERR_UNAUTHORIZED) + (asserts! (not (get isActive job)) ERR_JOB_IS_ACTIVE) + (map-set Jobs + jobId + (merge job { isActive: true }) + ) + (ok true) + ) +) + +(define-public (approve-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + (previousVote (map-get? JobApprovers { jobId: jobId, approver: tx-sender })) + ) + (asserts! (get isActive job) ERR_JOB_IS_NOT_ACTIVE) + (asserts! (is-approver tx-sender) ERR_UNAUTHORIZED) + ;; save vote + (map-set JobApprovers + { jobId: jobId, approver: tx-sender } + true + ) + (match previousVote approved + (begin + (asserts! (not approved) ERR_ALREADY_VOTED_THIS_WAY) + (map-set Jobs jobId + (merge job + { + approvals: (+ (get approvals job) u1), + disapprovals: (- (get disapprovals job) u1) + } + ) + ) + ) + ;; no previous vote + (map-set Jobs + jobId + (merge job { approvals: (+ (get approvals job) u1) } ) + ) + ) + (ok true) + ) +) + +(define-public (disapprove-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + (previousVote (map-get? JobApprovers { jobId: jobId, approver: tx-sender })) + ) + (asserts! (get isActive job) ERR_JOB_IS_NOT_ACTIVE) + (asserts! (is-approver tx-sender) ERR_UNAUTHORIZED) + ;; save vote + (map-set JobApprovers + { jobId: jobId, approver: tx-sender } + false + ) + (match previousVote approved + (begin + (asserts! approved ERR_ALREADY_VOTED_THIS_WAY) + (map-set Jobs jobId + (merge job + { + approvals: (- (get approvals job) u1), + disapprovals: (+ (get disapprovals job) u1) + } + ) + ) + ) + ;; no previous vote + (map-set Jobs + jobId + (merge job { disapprovals: (+ (get disapprovals job) u1) } ) + ) + ) + (ok true) + ) +) + +(define-read-only (is-job-approved (jobId uint)) + (match (get-job jobId) job + (>= (get approvals job) REQUIRED_APPROVALS) + false + ) +) + +(define-public (mark-job-as-executed (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + ) + (asserts! (get isActive job) ERR_JOB_IS_NOT_ACTIVE) + (asserts! (>= (get approvals job) REQUIRED_APPROVALS) ERR_JOB_IS_NOT_APPROVED) + (asserts! (is-eq (get target job) contract-caller) ERR_UNAUTHORIZED) + (asserts! (not (get isExecuted job)) ERR_JOB_IS_EXECUTED) + (map-set Jobs + jobId + (merge job { isExecuted: true }) + ) + (ok true) + ) +) + +(define-public (add-uint-argument (jobId uint) (argumentName (string-ascii 255)) (value uint)) + (let + ( + (argumentId (generate-argument-id jobId "uint")) + ) + (try! (guard-add-argument jobId)) + (asserts! + (and + (map-insert UIntArgumentsById + { jobId: jobId, argumentId: argumentId } + { argumentName: argumentName, value: value } + ) + (map-insert UIntArgumentsByName + { jobId: jobId, argumentName: argumentName } + { argumentId: argumentId, value: value} + ) + ) + ERR_ARGUMENT_ALREADY_EXISTS + ) + (ok true) + ) +) + +(define-read-only (get-uint-argument-by-name (jobId uint) (argumentName (string-ascii 255))) + (map-get? UIntArgumentsByName { jobId: jobId, argumentName: argumentName }) +) + +(define-read-only (get-uint-argument-by-id (jobId uint) (argumentId uint)) + (map-get? UIntArgumentsById { jobId: jobId, argumentId: argumentId }) +) + +(define-read-only (get-uint-value-by-name (jobId uint) (argumentName (string-ascii 255))) + (get value (get-uint-argument-by-name jobId argumentName)) +) + +(define-read-only (get-uint-value-by-id (jobId uint) (argumentId uint)) + (get value (get-uint-argument-by-id jobId argumentId)) +) + +(define-public (add-principal-argument (jobId uint) (argumentName (string-ascii 255)) (value principal)) + (let + ( + (argumentId (generate-argument-id jobId "principal")) + ) + (try! (guard-add-argument jobId)) + (asserts! + (and + (map-insert PrincipalArgumentsById + { jobId: jobId, argumentId: argumentId } + { argumentName: argumentName, value: value } + ) + (map-insert PrincipalArgumentsByName + { jobId: jobId, argumentName: argumentName } + { argumentId: argumentId, value: value} + ) + ) + ERR_ARGUMENT_ALREADY_EXISTS + ) + (ok true) + ) +) + +(define-read-only (get-principal-argument-by-name (jobId uint) (argumentName (string-ascii 255))) + (map-get? PrincipalArgumentsByName { jobId: jobId, argumentName: argumentName }) +) + +(define-read-only (get-principal-argument-by-id (jobId uint) (argumentId uint)) + (map-get? PrincipalArgumentsById { jobId: jobId, argumentId: argumentId }) +) + +(define-read-only (get-principal-value-by-name (jobId uint) (argumentName (string-ascii 255))) + (get value (get-principal-argument-by-name jobId argumentName)) +) + +(define-read-only (get-principal-value-by-id (jobId uint) (argumentId uint)) + (get value (get-principal-argument-by-id jobId argumentId)) +) + +;; PRIVATE FUNCTIONS + +(define-read-only (is-approver (user principal)) + (default-to false (map-get? Approvers user)) +) + +(define-private (generate-argument-id (jobId uint) (argumentType (string-ascii 25))) + (let + ( + (argumentId (+ (default-to u0 (map-get? ArgumentLastIdsByType { jobId: jobId, argumentType: argumentType })) u1)) + ) + (map-set ArgumentLastIdsByType + { jobId: jobId, argumentType: argumentType } + argumentId + ) + ;; return + argumentId + ) +) + +(define-private (guard-add-argument (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + ) + (asserts! (not (get isActive job)) ERR_JOB_IS_ACTIVE) + (asserts! (is-eq (get creator job) contract-caller) ERR_UNAUTHORIZED) + (ok true) + ) +) + +;; CONTRACT MANAGEMENT + +;; initial value for active core contract +;; set to deployer address at startup to prevent +;; circular dependency of core on auth +(define-data-var activeCoreContract principal CONTRACT_OWNER) +(define-data-var initialized bool false) + +;; core contract states +(define-constant STATE_DEPLOYED u0) +(define-constant STATE_ACTIVE u1) +(define-constant STATE_INACTIVE u2) + +;; core contract map +(define-map CoreContracts + principal + { + state: uint, + startHeight: uint, + endHeight: uint + } +) + +;; getter for active core contract +(define-read-only (get-active-core-contract) + (begin + (asserts! (not (is-eq (var-get activeCoreContract) CONTRACT_OWNER)) ERR_NO_ACTIVE_CORE_CONTRACT) + (ok (var-get activeCoreContract)) + ) +) + +;; getter for core contract map +(define-read-only (get-core-contract-info (targetContract principal)) + (let + ( + (coreContract (unwrap! (map-get? CoreContracts targetContract) ERR_CORE_CONTRACT_NOT_FOUND)) + ) + (ok coreContract) + ) +) + +;; one-time function to initialize contracts after all contracts are deployed +;; - check that deployer is calling this function +;; - check this contract is not activated already (one-time use) +;; - set initial map value for core contract v1 +;; - set cityWallet in core contract +;; - set intialized true +(define-public (initialize-contracts (coreContract )) + (let + ( + (coreContractAddress (contract-of coreContract)) + ) + (asserts! (is-eq contract-caller CONTRACT_OWNER) ERR_UNAUTHORIZED) + (asserts! (not (var-get initialized)) ERR_UNAUTHORIZED) + (map-set CoreContracts + coreContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (try! (contract-call? coreContract set-city-wallet (var-get cityWallet))) + (var-set initialized true) + (ok true) + ) +) + +(define-read-only (is-initialized) + (var-get initialized) +) + +;; function to activate core contract through registration +;; - check that target is in core contract map +;; - check that caller is core contract +;; - check that target is in STATE_DEPLOYED +;; - set active in core contract map +;; - set as activeCoreContract +(define-public (activate-core-contract (targetContract principal) (stacksHeight uint)) + (let + ( + (coreContract (unwrap! (map-get? CoreContracts targetContract) ERR_CORE_CONTRACT_NOT_FOUND)) + ) + (asserts! (is-eq (get state coreContract) STATE_DEPLOYED) ERR_INCORRECT_CONTRACT_STATE) + (asserts! (is-eq contract-caller targetContract) ERR_UNAUTHORIZED) + (map-set CoreContracts + targetContract + { + state: STATE_ACTIVE, + startHeight: stacksHeight, + endHeight: u0 + }) + (var-set activeCoreContract targetContract) + (ok true) + ) +) + +;; protected function to update core contract +(define-public (upgrade-core-contract (oldContract ) (newContract )) + (let + ( + (oldContractAddress (contract-of oldContract)) + (oldContractMap (unwrap! (map-get? CoreContracts oldContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + (newContractAddress (contract-of newContract)) + ) + (asserts! (not (is-eq oldContractAddress newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (is-none (map-get? CoreContracts newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + (map-set CoreContracts + oldContractAddress + { + state: STATE_INACTIVE, + startHeight: (get startHeight oldContractMap), + endHeight: block-height + }) + (map-set CoreContracts + newContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (var-set activeCoreContract newContractAddress) + (try! (contract-call? oldContract shutdown-contract block-height)) + (try! (contract-call? newContract set-city-wallet (var-get cityWallet))) + (ok true) + ) +) + +(define-public (execute-upgrade-core-contract-job (jobId uint) (oldContract ) (newContract )) + (let + ( + (oldContractArg (unwrap! (get-principal-value-by-name jobId "oldContract") ERR_UNKNOWN_ARGUMENT)) + (newContractArg (unwrap! (get-principal-value-by-name jobId "newContract") ERR_UNKNOWN_ARGUMENT)) + (oldContractAddress (contract-of oldContract)) + (oldContractMap (unwrap! (map-get? CoreContracts oldContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + (newContractAddress (contract-of newContract)) + ) + (asserts! (not (is-eq oldContractAddress newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (is-none (map-get? CoreContracts newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (and (is-eq oldContractArg oldContractAddress) (is-eq newContractArg newContractAddress)) ERR_UNAUTHORIZED) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (map-set CoreContracts + oldContractAddress + { + state: STATE_INACTIVE, + startHeight: (get startHeight oldContractMap), + endHeight: block-height + }) + (map-set CoreContracts + newContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (var-set activeCoreContract newContractAddress) + (try! (contract-call? oldContract shutdown-contract block-height)) + (try! (contract-call? newContract set-city-wallet (var-get cityWallet))) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; CITY WALLET MANAGEMENT + +;; initial value for city wallet +(define-data-var cityWallet principal 'ST3PM583Q21NF0GB428P79VFPYH8X5DQVKDDGD74T) + +;; returns city wallet principal +(define-read-only (get-city-wallet) + (ok (var-get cityWallet)) +) + +;; protected function to update city wallet variable +(define-public (set-city-wallet (targetContract ) (newCityWallet principal)) + (let + ( + (coreContractAddress (contract-of targetContract)) + (coreContract (unwrap! (map-get? CoreContracts coreContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + ) + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + (asserts! (is-eq coreContractAddress (var-get activeCoreContract)) ERR_UNAUTHORIZED) + (var-set cityWallet newCityWallet) + (try! (contract-call? targetContract set-city-wallet newCityWallet)) + (ok true) + ) +) + +(define-public (execute-set-city-wallet-job (jobId uint) (targetContract )) + (let + ( + (coreContractAddress (contract-of targetContract)) + (coreContract (unwrap! (map-get? CoreContracts coreContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + (newCityWallet (unwrap! (get-principal-value-by-name jobId "newCityWallet") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (asserts! (is-eq coreContractAddress (var-get activeCoreContract)) ERR_UNAUTHORIZED) + (var-set cityWallet newCityWallet) + (try! (contract-call? targetContract set-city-wallet newCityWallet)) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; check if contract caller is city wallet +(define-private (is-authorized-city) + (is-eq contract-caller (var-get cityWallet)) +) + +;; TOKEN MANAGEMENT + +(define-public (set-token-uri (targetContract ) (newUri (optional (string-utf8 256)))) + (begin + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + (as-contract (try! (contract-call? targetContract set-token-uri newUri))) + (ok true) + ) +) + +;; COINBASE THRESHOLDS + +(define-public (update-coinbase-thresholds (targetCore ) (targetToken ) (threshold1 uint) (threshold2 uint) (threshold3 uint) (threshold4 uint) (threshold5 uint)) + (begin + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + ;; update in token contract + (as-contract (try! (contract-call? targetToken update-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5))) + ;; update core contract based on token contract + (as-contract (try! (contract-call? targetCore update-coinbase-thresholds))) + (ok true) + ) +) + +(define-public (execute-update-coinbase-thresholds-job (jobId uint) (targetCore ) (targetToken )) + (let + ( + (threshold1 (unwrap! (get-uint-value-by-name jobId "threshold1") ERR_UNKNOWN_ARGUMENT)) + (threshold2 (unwrap! (get-uint-value-by-name jobId "threshold2") ERR_UNKNOWN_ARGUMENT)) + (threshold3 (unwrap! (get-uint-value-by-name jobId "threshold3") ERR_UNKNOWN_ARGUMENT)) + (threshold4 (unwrap! (get-uint-value-by-name jobId "threshold4") ERR_UNKNOWN_ARGUMENT)) + (threshold5 (unwrap! (get-uint-value-by-name jobId "threshold5") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (as-contract (try! (contract-call? targetToken update-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5))) + (as-contract (try! (contract-call? targetCore update-coinbase-thresholds))) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; COINBASE AMOUNTS (REWARDS) + +(define-public (update-coinbase-amounts (targetCore ) (targetToken ) (amountBonus uint) (amount1 uint) (amount2 uint) (amount3 uint) (amount4 uint) (amount5 uint) (amountDefault uint)) + (begin + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + ;; update in token contract + (as-contract (try! (contract-call? targetToken update-coinbase-amounts amountBonus amount1 amount2 amount3 amount4 amount5 amountDefault))) + ;; update core contract based on token contract + (as-contract (try! (contract-call? targetCore update-coinbase-amounts))) + (ok true) + ) +) + +(define-public (execute-update-coinbase-amounts-job (jobId uint) (targetCore ) (targetToken )) + (let + ( + (amountBonus (unwrap! (get-uint-value-by-name jobId "amountBonus") ERR_UNKNOWN_ARGUMENT)) + (amount1 (unwrap! (get-uint-value-by-name jobId "amount1") ERR_UNKNOWN_ARGUMENT)) + (amount2 (unwrap! (get-uint-value-by-name jobId "amount2") ERR_UNKNOWN_ARGUMENT)) + (amount3 (unwrap! (get-uint-value-by-name jobId "amount3") ERR_UNKNOWN_ARGUMENT)) + (amount4 (unwrap! (get-uint-value-by-name jobId "amount4") ERR_UNKNOWN_ARGUMENT)) + (amount5 (unwrap! (get-uint-value-by-name jobId "amount5") ERR_UNKNOWN_ARGUMENT)) + (amountDefault (unwrap! (get-uint-value-by-name jobId "amountDefault") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (as-contract (try! (contract-call? targetToken update-coinbase-amounts amountBonus amount1 amount2 amount3 amount4 amount5 amountDefault))) + (as-contract (try! (contract-call? targetCore update-coinbase-amounts))) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; APPROVERS MANAGEMENT + +(define-public (execute-replace-approver-job (jobId uint)) + (let + ( + (oldApprover (unwrap! (get-principal-value-by-name jobId "oldApprover") ERR_UNKNOWN_ARGUMENT)) + (newApprover (unwrap! (get-principal-value-by-name jobId "newApprover") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (map-set Approvers oldApprover false) + (map-set Approvers newApprover true) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; CONTRACT INITIALIZATION + +(map-insert Approvers 'ST3AY0CM7SD9183QZ4Y7S2RGBZX9GQT54MJ6XY0BN true) +(map-insert Approvers 'ST2D06VFWWTNCWHVB2FJ9KJ3EB30HFRTHB1A4BSP3 true) +(map-insert Approvers 'ST113N3MMPZRMJJRZH6JTHA5CB7TBZH1EH4C22GFV true) +(map-insert Approvers 'ST8YRW1THF2XT8E45XXCGYKZH2B70HYH71VC7737 true) +(map-insert Approvers 'STX13Q7ZJDSFVDZMQ1PWDFGT4QSBMASRMCYE4NAP true) diff --git a/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-auth-v2.json b/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-auth-v2.json new file mode 100644 index 00000000..89c8a70a --- /dev/null +++ b/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-auth-v2.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-core-v2.clar b/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-core-v2.clar new file mode 100644 index 00000000..cd5403d1 --- /dev/null +++ b/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-core-v2.clar @@ -0,0 +1,1007 @@ +;; MIAMICOIN CORE CONTRACT V2 TESTNET +;; CityCoins Protocol Version 2.0.0 + +;; GENERAL CONFIGURATION + +(impl-trait 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-core-v2-trait.citycoin-core-v2) +(define-constant CONTRACT_OWNER tx-sender) + +;; ERROR CODES + +(define-constant ERR_UNAUTHORIZED (err u1000)) +(define-constant ERR_USER_ALREADY_REGISTERED (err u1001)) +(define-constant ERR_USER_NOT_FOUND (err u1002)) +(define-constant ERR_USER_ID_NOT_FOUND (err u1003)) +(define-constant ERR_ACTIVATION_THRESHOLD_REACHED (err u1004)) +(define-constant ERR_CONTRACT_NOT_ACTIVATED (err u1005)) +(define-constant ERR_USER_ALREADY_MINED (err u1006)) +(define-constant ERR_INSUFFICIENT_COMMITMENT (err u1007)) +(define-constant ERR_INSUFFICIENT_BALANCE (err u1008)) +(define-constant ERR_USER_DID_NOT_MINE_IN_BLOCK (err u1009)) +(define-constant ERR_CLAIMED_BEFORE_MATURITY (err u1010)) +(define-constant ERR_NO_MINERS_AT_BLOCK (err u1011)) +(define-constant ERR_REWARD_ALREADY_CLAIMED (err u1012)) +(define-constant ERR_MINER_DID_NOT_WIN (err u1013)) +(define-constant ERR_NO_VRF_SEED_FOUND (err u1014)) +(define-constant ERR_STACKING_NOT_AVAILABLE (err u1015)) +(define-constant ERR_CANNOT_STACK (err u1016)) +(define-constant ERR_REWARD_CYCLE_NOT_COMPLETED (err u1017)) +(define-constant ERR_NOTHING_TO_REDEEM (err u1018)) +(define-constant ERR_UNABLE_TO_FIND_CITY_WALLET (err u1019)) +(define-constant ERR_CLAIM_IN_WRONG_CONTRACT (err u1020)) +(define-constant ERR_BLOCK_HEIGHT_IN_PAST (err u1021)) +(define-constant ERR_COINBASE_AMOUNTS_NOT_FOUND (err u1022)) + +;; CITY WALLET MANAGEMENT + +;; initial value for city wallet, set to this contract until initialized +(define-data-var cityWallet principal 'ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-core-v2) + +;; returns set city wallet principal +(define-read-only (get-city-wallet) + (var-get cityWallet) +) + +;; protected function to update city wallet variable +(define-public (set-city-wallet (newCityWallet principal)) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (ok (var-set cityWallet newCityWallet)) + ) +) + +;; REGISTRATION + +(define-constant MIAMICOIN_ACTIVATION_HEIGHT u24497) +(define-data-var activationBlock uint u340282366920938463463374607431768211455) +(define-data-var activationDelay uint u1) ;; TESTNET: set to 1 block +(define-data-var activationReached bool false) +(define-data-var activationTarget uint u0) +(define-data-var activationThreshold uint u2) ;; TESTNET: set to 2 users +(define-data-var usersNonce uint u0) + +;; returns Stacks block height registration was activated at plus activationDelay +(define-read-only (get-activation-block) + (begin + (asserts! (get-activation-status) ERR_CONTRACT_NOT_ACTIVATED) + (ok (var-get activationBlock)) + ) +) + +;; returns activation delay +(define-read-only (get-activation-delay) + (var-get activationDelay) +) + +;; returns activation status as boolean +(define-read-only (get-activation-status) + (var-get activationReached) +) + +;; returns activation target +(define-read-only (get-activation-target) + (begin + (asserts! (get-activation-status) ERR_CONTRACT_NOT_ACTIVATED) + (ok (var-get activationTarget)) + ) +) + +;; returns activation threshold +(define-read-only (get-activation-threshold) + (var-get activationThreshold) +) + +;; returns number of registered users, used for activation and tracking user IDs +(define-read-only (get-registered-users-nonce) + (var-get usersNonce) +) + +;; store user principal by user id +(define-map Users + uint + principal +) + +;; store user id by user principal +(define-map UserIds + principal + uint +) + +;; returns (some userId) or none +(define-read-only (get-user-id (user principal)) + (map-get? UserIds user) +) + +;; returns (some userPrincipal) or none +(define-read-only (get-user (userId uint)) + (map-get? Users userId) +) + +;; returns user ID if it has been created, or creates and returns new ID +(define-private (get-or-create-user-id (user principal)) + (match + (map-get? UserIds user) + value value + (let + ( + (newId (+ u1 (var-get usersNonce))) + ) + (map-set Users newId user) + (map-set UserIds user newId) + (var-set usersNonce newId) + newId + ) + ) +) + +;; registers users that signal activation of contract until threshold is met +(define-public (register-user (memo (optional (string-utf8 50)))) + (let + ( + (newId (+ u1 (var-get usersNonce))) + (threshold (var-get activationThreshold)) + (initialized (contract-call? 'ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-auth-v2 is-initialized)) + ) + + (asserts! initialized ERR_UNAUTHORIZED) + + (asserts! (is-none (map-get? UserIds tx-sender)) + ERR_USER_ALREADY_REGISTERED) + + (asserts! (<= newId threshold) + ERR_ACTIVATION_THRESHOLD_REACHED) + + (if (is-some memo) + (print memo) + none + ) + + (get-or-create-user-id tx-sender) + + (if (is-eq newId threshold) + (let + ( + (activationTargetBlock (+ block-height (var-get activationDelay))) + ) + (try! (contract-call? 'ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-auth-v2 activate-core-contract (as-contract tx-sender) activationTargetBlock)) + (try! (contract-call? 'ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-token-v2 activate-token (as-contract tx-sender) MIAMICOIN_ACTIVATION_HEIGHT)) + (try! (set-coinbase-thresholds)) + (try! (set-coinbase-amounts)) + (var-set activationReached true) + (var-set activationBlock MIAMICOIN_ACTIVATION_HEIGHT) + (var-set activationTarget activationTargetBlock) + (ok true) + ) + (ok true) + ) + ) +) + +;; MINING CONFIGURATION + +;; define split to custodied wallet address for the city +(define-constant SPLIT_CITY_PCT u30) + +;; how long a miner must wait before block winner can claim their minted tokens +(define-data-var tokenRewardMaturity uint u10) ;; TESTNET: set to 10 blocks + +;; At a given Stacks block height: +;; - how many miners were there +;; - what was the total amount submitted +;; - what was the total amount submitted to the city +;; - what was the total amount submitted to Stackers +;; - was the block reward claimed +(define-map MiningStatsAtBlock + uint + { + minersCount: uint, + amount: uint, + amountToCity: uint, + amountToStackers: uint, + rewardClaimed: bool + } +) + +;; returns map MiningStatsAtBlock at a given Stacks block height if it exists +(define-read-only (get-mining-stats-at-block (stacksHeight uint)) + (map-get? MiningStatsAtBlock stacksHeight) +) + +;; returns map MiningStatsAtBlock at a given Stacks block height +;; or, an empty structure +(define-read-only (get-mining-stats-at-block-or-default (stacksHeight uint)) + (default-to { + minersCount: u0, + amount: u0, + amountToCity: u0, + amountToStackers: u0, + rewardClaimed: false + } + (map-get? MiningStatsAtBlock stacksHeight) + ) +) + +;; At a given Stacks block height and user ID: +;; - what is their ustx commitment +;; - what are the low/high values (used for VRF) +(define-map MinersAtBlock + { + stacksHeight: uint, + userId: uint + } + { + ustx: uint, + lowValue: uint, + highValue: uint, + winner: bool + } +) + +;; returns true if a given miner has already mined at a given block height +(define-read-only (has-mined-at-block (stacksHeight uint) (userId uint)) + (is-some + (map-get? MinersAtBlock { stacksHeight: stacksHeight, userId: userId }) + ) +) + +;; returns map MinersAtBlock at a given Stacks block height for a user ID +(define-read-only (get-miner-at-block (stacksHeight uint) (userId uint)) + (map-get? MinersAtBlock { stacksHeight: stacksHeight, userId: userId }) +) + +;; returns map MinersAtBlock at a given Stacks block height for a user ID +;; or, an empty structure +(define-read-only (get-miner-at-block-or-default (stacksHeight uint) (userId uint)) + (default-to { + highValue: u0, + lowValue: u0, + ustx: u0, + winner: false + } + (map-get? MinersAtBlock { stacksHeight: stacksHeight, userId: userId })) +) + +;; At a given Stacks block height: +;; - what is the max highValue from MinersAtBlock (used for VRF) +(define-map MinersAtBlockHighValue + uint + uint +) + +;; returns last high value from map MinersAtBlockHighValue +(define-read-only (get-last-high-value-at-block (stacksHeight uint)) + (default-to u0 + (map-get? MinersAtBlockHighValue stacksHeight)) +) + +;; At a given Stacks block height: +;; - what is the userId of miner who won this block +(define-map BlockWinnerIds + uint + uint +) + +(define-read-only (get-block-winner-id (stacksHeight uint)) + (map-get? BlockWinnerIds stacksHeight) +) + +;; MINING ACTIONS + +(define-public (mine-tokens (amountUstx uint) (memo (optional (buff 34)))) + (let + ( + (userId (get-or-create-user-id tx-sender)) + ) + (try! (mine-tokens-at-block userId block-height amountUstx memo)) + (ok true) + ) +) + +(define-public (mine-many (amounts (list 200 uint))) + (begin + (asserts! (is-activated) ERR_CONTRACT_NOT_ACTIVATED) + (asserts! (> (len amounts) u0) ERR_INSUFFICIENT_COMMITMENT) + (match (fold mine-single amounts (ok { userId: (get-or-create-user-id tx-sender), toStackers: u0, toCity: u0, stacksHeight: block-height })) + okReturn + (begin + (asserts! (>= (stx-get-balance tx-sender) (+ (get toStackers okReturn) (get toCity okReturn))) ERR_INSUFFICIENT_BALANCE) + (if (> (get toStackers okReturn ) u0) + (try! (stx-transfer? (get toStackers okReturn ) tx-sender (as-contract tx-sender))) + false + ) + (try! (stx-transfer? (get toCity okReturn) tx-sender (var-get cityWallet))) + (print { + firstBlock: block-height, + lastBlock: (- (+ block-height (len amounts)) u1) + }) + (ok true) + ) + errReturn (err errReturn) + ) + ) +) + +(define-private (mine-single + (amountUstx uint) + (return (response + { + userId: uint, + toStackers: uint, + toCity: uint, + stacksHeight: uint + } + uint + ))) + + (match return okReturn + (let + ( + (stacksHeight (get stacksHeight okReturn)) + (rewardCycle (default-to u0 (get-reward-cycle stacksHeight))) + (stackingActive (stacking-active-at-cycle rewardCycle)) + (toCity + (if stackingActive + (/ (* SPLIT_CITY_PCT amountUstx) u100) + amountUstx + ) + ) + (toStackers (- amountUstx toCity)) + ) + (asserts! (not (has-mined-at-block stacksHeight (get userId okReturn))) ERR_USER_ALREADY_MINED) + (asserts! (> amountUstx u0) ERR_INSUFFICIENT_COMMITMENT) + (try! (set-tokens-mined (get userId okReturn) stacksHeight amountUstx toStackers toCity)) + (ok (merge okReturn + { + toStackers: (+ (get toStackers okReturn) toStackers), + toCity: (+ (get toCity okReturn) toCity), + stacksHeight: (+ stacksHeight u1) + } + )) + ) + errReturn (err errReturn) + ) +) + +(define-private (mine-tokens-at-block (userId uint) (stacksHeight uint) (amountUstx uint) (memo (optional (buff 34)))) + (let + ( + (rewardCycle (default-to u0 (get-reward-cycle stacksHeight))) + (stackingActive (stacking-active-at-cycle rewardCycle)) + (toCity + (if stackingActive + (/ (* SPLIT_CITY_PCT amountUstx) u100) + amountUstx + ) + ) + (toStackers (- amountUstx toCity)) + ) + (asserts! (is-activated) ERR_CONTRACT_NOT_ACTIVATED) + (asserts! (not (has-mined-at-block stacksHeight userId)) ERR_USER_ALREADY_MINED) + (asserts! (> amountUstx u0) ERR_INSUFFICIENT_COMMITMENT) + (asserts! (>= (stx-get-balance tx-sender) amountUstx) ERR_INSUFFICIENT_BALANCE) + (try! (set-tokens-mined userId stacksHeight amountUstx toStackers toCity)) + (if (is-some memo) + (print memo) + none + ) + (if stackingActive + (try! (stx-transfer? toStackers tx-sender (as-contract tx-sender))) + false + ) + (try! (stx-transfer? toCity tx-sender (var-get cityWallet))) + (ok true) + ) +) + +(define-private (set-tokens-mined (userId uint) (stacksHeight uint) (amountUstx uint) (toStackers uint) (toCity uint)) + (let + ( + (blockStats (get-mining-stats-at-block-or-default stacksHeight)) + (newMinersCount (+ (get minersCount blockStats) u1)) + (minerLowVal (get-last-high-value-at-block stacksHeight)) + (rewardCycle (unwrap! (get-reward-cycle stacksHeight) + ERR_STACKING_NOT_AVAILABLE)) + (rewardCycleStats (get-stacking-stats-at-cycle-or-default rewardCycle)) + ) + (map-set MiningStatsAtBlock + stacksHeight + { + minersCount: newMinersCount, + amount: (+ (get amount blockStats) amountUstx), + amountToCity: (+ (get amountToCity blockStats) toCity), + amountToStackers: (+ (get amountToStackers blockStats) toStackers), + rewardClaimed: false + } + ) + (map-set MinersAtBlock + { + stacksHeight: stacksHeight, + userId: userId + } + { + ustx: amountUstx, + lowValue: (if (> minerLowVal u0) (+ minerLowVal u1) u0), + highValue: (+ minerLowVal amountUstx), + winner: false + } + ) + (map-set MinersAtBlockHighValue + stacksHeight + (+ minerLowVal amountUstx) + ) + (if (> toStackers u0) + (map-set StackingStatsAtCycle + rewardCycle + { + amountUstx: (+ (get amountUstx rewardCycleStats) toStackers), + amountToken: (get amountToken rewardCycleStats) + } + ) + false + ) + (ok true) + ) +) + +;; MINING REWARD CLAIM ACTIONS + +;; calls function to claim mining reward in active logic contract +(define-public (claim-mining-reward (minerBlockHeight uint)) + (begin + (asserts! (or (is-eq (var-get shutdownHeight) u0) (< minerBlockHeight (var-get shutdownHeight))) ERR_CLAIM_IN_WRONG_CONTRACT) + (try! (claim-mining-reward-at-block tx-sender block-height minerBlockHeight)) + (ok true) + ) +) + +;; Determine whether or not the given principal can claim the mined tokens at a particular block height, +;; given the miners record for that block height, a random sample, and the current block height. +(define-private (claim-mining-reward-at-block (user principal) (stacksHeight uint) (minerBlockHeight uint)) + (let + ( + (maturityHeight (+ (var-get tokenRewardMaturity) minerBlockHeight)) + (userId (unwrap! (get-user-id user) ERR_USER_ID_NOT_FOUND)) + (blockStats (unwrap! (get-mining-stats-at-block minerBlockHeight) ERR_NO_MINERS_AT_BLOCK)) + (minerStats (unwrap! (get-miner-at-block minerBlockHeight userId) ERR_USER_DID_NOT_MINE_IN_BLOCK)) + (isMature (asserts! (> stacksHeight maturityHeight) ERR_CLAIMED_BEFORE_MATURITY)) + (vrfSample (unwrap! (contract-call? 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-vrf-v2 get-save-rnd maturityHeight) ERR_NO_VRF_SEED_FOUND)) + (commitTotal (get-last-high-value-at-block minerBlockHeight)) + (winningValue (mod vrfSample commitTotal)) + ) + (asserts! (not (get rewardClaimed blockStats)) ERR_REWARD_ALREADY_CLAIMED) + (asserts! (and (>= winningValue (get lowValue minerStats)) (<= winningValue (get highValue minerStats))) + ERR_MINER_DID_NOT_WIN) + (try! (set-mining-reward-claimed userId minerBlockHeight)) + (ok true) + ) +) + +(define-private (set-mining-reward-claimed (userId uint) (minerBlockHeight uint)) + (let + ( + (blockStats (get-mining-stats-at-block-or-default minerBlockHeight)) + (minerStats (get-miner-at-block-or-default minerBlockHeight userId)) + (user (unwrap! (get-user userId) ERR_USER_NOT_FOUND)) + ) + (map-set MiningStatsAtBlock + minerBlockHeight + { + minersCount: (get minersCount blockStats), + amount: (get amount blockStats), + amountToCity: (get amountToCity blockStats), + amountToStackers: (get amountToStackers blockStats), + rewardClaimed: true + } + ) + (map-set MinersAtBlock + { + stacksHeight: minerBlockHeight, + userId: userId + } + { + ustx: (get ustx minerStats), + lowValue: (get lowValue minerStats), + highValue: (get highValue minerStats), + winner: true + } + ) + (map-set BlockWinnerIds + minerBlockHeight + userId + ) + (try! (mint-coinbase user minerBlockHeight)) + (ok true) + ) +) + +(define-read-only (is-block-winner (user principal) (minerBlockHeight uint)) + (is-block-winner-and-can-claim user minerBlockHeight false) +) + +(define-read-only (can-claim-mining-reward (user principal) (minerBlockHeight uint)) + (is-block-winner-and-can-claim user minerBlockHeight true) +) + +(define-private (is-block-winner-and-can-claim (user principal) (minerBlockHeight uint) (testCanClaim bool)) + (let + ( + (userId (unwrap! (get-user-id user) false)) + (blockStats (unwrap! (get-mining-stats-at-block minerBlockHeight) false)) + (minerStats (unwrap! (get-miner-at-block minerBlockHeight userId) false)) + (maturityHeight (+ (var-get tokenRewardMaturity) minerBlockHeight)) + (vrfSample (unwrap! (contract-call? 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-vrf-v2 get-rnd maturityHeight) false)) + (commitTotal (get-last-high-value-at-block minerBlockHeight)) + (winningValue (mod vrfSample commitTotal)) + ) + (if (and (>= winningValue (get lowValue minerStats)) (<= winningValue (get highValue minerStats))) + (if testCanClaim (not (get rewardClaimed blockStats)) true) + false + ) + ) +) + +;; STACKING CONFIGURATION + +(define-constant MAX_REWARD_CYCLES u32) +(define-constant REWARD_CYCLE_INDEXES (list u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11 u12 u13 u14 u15 u16 u17 u18 u19 u20 u21 u22 u23 u24 u25 u26 u27 u28 u29 u30 u31)) + +;; how long a reward cycle is +(define-data-var rewardCycleLength uint u20) ;; TESTNET: set to 20 blocks + +;; At a given reward cycle: +;; - how many Stackers were there +;; - what is the total uSTX submitted by miners +;; - what is the total amount of tokens stacked +(define-map StackingStatsAtCycle + uint + { + amountUstx: uint, + amountToken: uint + } +) + +;; returns the total stacked tokens and committed uSTX for a given reward cycle +(define-read-only (get-stacking-stats-at-cycle (rewardCycle uint)) + (map-get? StackingStatsAtCycle rewardCycle) +) + +;; returns the total stacked tokens and committed uSTX for a given reward cycle +;; or, an empty structure +(define-read-only (get-stacking-stats-at-cycle-or-default (rewardCycle uint)) + (default-to { amountUstx: u0, amountToken: u0 } + (map-get? StackingStatsAtCycle rewardCycle)) +) + +;; At a given reward cycle and user ID: +;; - what is the total tokens Stacked? +;; - how many tokens should be returned? (based on Stacking period) +(define-map StackerAtCycle + { + rewardCycle: uint, + userId: uint + } + { + amountStacked: uint, + toReturn: uint + } +) + +(define-read-only (get-stacker-at-cycle (rewardCycle uint) (userId uint)) + (map-get? StackerAtCycle { rewardCycle: rewardCycle, userId: userId }) +) + +(define-read-only (get-stacker-at-cycle-or-default (rewardCycle uint) (userId uint)) + (default-to { amountStacked: u0, toReturn: u0 } + (map-get? StackerAtCycle { rewardCycle: rewardCycle, userId: userId })) +) + +;; get the reward cycle for a given Stacks block height +(define-read-only (get-reward-cycle (stacksHeight uint)) + (let + ( + (firstStackingBlock (var-get activationBlock)) + (rcLen (var-get rewardCycleLength)) + ) + (if (>= stacksHeight firstStackingBlock) + (some (/ (- stacksHeight firstStackingBlock) rcLen)) + none) + ) +) + +;; determine if stacking is active in a given cycle +(define-read-only (stacking-active-at-cycle (rewardCycle uint)) + (is-some + (get amountToken (map-get? StackingStatsAtCycle rewardCycle)) + ) +) + +;; get the first Stacks block height for a given reward cycle. +(define-read-only (get-first-stacks-block-in-reward-cycle (rewardCycle uint)) + (+ (var-get activationBlock) (* (var-get rewardCycleLength) rewardCycle)) +) + +;; getter for get-entitled-stacking-reward that specifies block height +(define-read-only (get-stacking-reward (userId uint) (targetCycle uint)) + (get-entitled-stacking-reward userId targetCycle block-height) +) + +;; get uSTX a Stacker can claim, given reward cycle they stacked in and current block height +;; this method only returns a positive value if: +;; - the current block height is in a subsequent reward cycle +;; - the stacker actually locked up tokens in the target reward cycle +;; - the stacker locked up _enough_ tokens to get at least one uSTX +;; it is possible to Stack tokens and not receive uSTX: +;; - if no miners commit during this reward cycle +;; - the amount stacked by user is too few that you'd be entitled to less than 1 uSTX +(define-private (get-entitled-stacking-reward (userId uint) (targetCycle uint) (stacksHeight uint)) + (let + ( + (rewardCycleStats (get-stacking-stats-at-cycle-or-default targetCycle)) + (stackerAtCycle (get-stacker-at-cycle-or-default targetCycle userId)) + (totalUstxThisCycle (get amountUstx rewardCycleStats)) + (totalStackedThisCycle (get amountToken rewardCycleStats)) + (userStackedThisCycle (get amountStacked stackerAtCycle)) + ) + (match (get-reward-cycle stacksHeight) + currentCycle + (if (and (not (var-get isShutdown)) + (or (<= currentCycle targetCycle) (is-eq u0 userStackedThisCycle))) + ;; the contract is not shut down and + ;; this cycle hasn't finished + ;; or stacker contributed nothing + u0 + ;; (totalUstxThisCycle * userStackedThisCycle) / totalStackedThisCycle + (/ (* totalUstxThisCycle userStackedThisCycle) totalStackedThisCycle) + ) + ;; before first reward cycle + u0 + ) + ) +) + +;; STACKING ACTIONS + +(define-public (stack-tokens (amountTokens uint) (lockPeriod uint)) + (let + ( + (userId (get-or-create-user-id tx-sender)) + ) + (try! (stack-tokens-at-cycle tx-sender userId amountTokens block-height lockPeriod)) + (ok true) + ) +) + +(define-private (stack-tokens-at-cycle (user principal) (userId uint) (amountTokens uint) (startHeight uint) (lockPeriod uint)) + (let + ( + (currentCycle (unwrap! (get-reward-cycle startHeight) ERR_STACKING_NOT_AVAILABLE)) + (targetCycle (+ u1 currentCycle)) + (commitment { + stackerId: userId, + amount: amountTokens, + first: targetCycle, + last: (+ targetCycle lockPeriod) + }) + ) + (asserts! (is-activated) ERR_CONTRACT_NOT_ACTIVATED) + (asserts! (and (> lockPeriod u0) (<= lockPeriod MAX_REWARD_CYCLES)) + ERR_CANNOT_STACK) + (asserts! (> amountTokens u0) ERR_CANNOT_STACK) + (try! (contract-call? 'ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-token-v2 transfer amountTokens tx-sender (as-contract tx-sender) none)) + (print { + firstCycle: targetCycle, + lastCycle: (- (+ targetCycle lockPeriod) u1) + }) + (match (fold stack-tokens-closure REWARD_CYCLE_INDEXES (ok commitment)) + okValue (ok true) + errValue (err errValue) + ) + ) +) + +(define-private (stack-tokens-closure (rewardCycleIdx uint) + (commitmentResponse (response + { + stackerId: uint, + amount: uint, + first: uint, + last: uint + } + uint + ))) + + (match commitmentResponse + commitment + (let + ( + (stackerId (get stackerId commitment)) + (amountToken (get amount commitment)) + (firstCycle (get first commitment)) + (lastCycle (get last commitment)) + (targetCycle (+ firstCycle rewardCycleIdx)) + ) + (begin + (if (and (>= targetCycle firstCycle) (< targetCycle lastCycle)) + (begin + (if (is-eq targetCycle (- lastCycle u1)) + (set-tokens-stacked stackerId targetCycle amountToken amountToken) + (set-tokens-stacked stackerId targetCycle amountToken u0) + ) + true + ) + false + ) + commitmentResponse + ) + ) + errValue commitmentResponse + ) +) + +(define-private (set-tokens-stacked (userId uint) (targetCycle uint) (amountStacked uint) (toReturn uint)) + (let + ( + (rewardCycleStats (get-stacking-stats-at-cycle-or-default targetCycle)) + (stackerAtCycle (get-stacker-at-cycle-or-default targetCycle userId)) + ) + (map-set StackingStatsAtCycle + targetCycle + { + amountUstx: (get amountUstx rewardCycleStats), + amountToken: (+ amountStacked (get amountToken rewardCycleStats)) + } + ) + (map-set StackerAtCycle + { + rewardCycle: targetCycle, + userId: userId + } + { + amountStacked: (+ amountStacked (get amountStacked stackerAtCycle)), + toReturn: (+ toReturn (get toReturn stackerAtCycle)) + } + ) + ) +) + +;; STACKING REWARD CLAIMS + +;; calls function to claim stacking reward in active logic contract +(define-public (claim-stacking-reward (targetCycle uint)) + (begin + (try! (claim-stacking-reward-at-cycle tx-sender block-height targetCycle)) + (ok true) + ) +) + +(define-private (claim-stacking-reward-at-cycle (user principal) (stacksHeight uint) (targetCycle uint)) + (let + ( + (currentCycle (unwrap! (get-reward-cycle stacksHeight) ERR_STACKING_NOT_AVAILABLE)) + (userId (unwrap! (get-user-id user) ERR_USER_ID_NOT_FOUND)) + (entitledUstx (get-entitled-stacking-reward userId targetCycle stacksHeight)) + (stackerAtCycle (get-stacker-at-cycle-or-default targetCycle userId)) + (toReturn (get toReturn stackerAtCycle)) + ) + (asserts! (or + (is-eq true (var-get isShutdown)) + (> currentCycle targetCycle)) + ERR_REWARD_CYCLE_NOT_COMPLETED) + (asserts! (or (> toReturn u0) (> entitledUstx u0)) ERR_NOTHING_TO_REDEEM) + ;; disable ability to claim again + (map-set StackerAtCycle + { + rewardCycle: targetCycle, + userId: userId + } + { + amountStacked: u0, + toReturn: u0 + } + ) + ;; send back tokens if user was eligible + (if (> toReturn u0) + (try! (as-contract (contract-call? 'ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-token-v2 transfer toReturn tx-sender user none))) + true + ) + ;; send back rewards if user was eligible + (if (> entitledUstx u0) + (try! (as-contract (stx-transfer? entitledUstx tx-sender user))) + true + ) + (ok true) + ) +) + +;; TOKEN CONFIGURATION + +;; decimals and multiplier for token +(define-constant DECIMALS u6) +(define-constant MICRO_CITYCOINS (pow u10 DECIMALS)) + +;; bonus period length for increased coinbase rewards +(define-constant TOKEN_BONUS_PERIOD u10000) + +;; coinbase thresholds per halving, used to determine halvings +(define-data-var coinbaseThreshold1 uint u0) +(define-data-var coinbaseThreshold2 uint u0) +(define-data-var coinbaseThreshold3 uint u0) +(define-data-var coinbaseThreshold4 uint u0) +(define-data-var coinbaseThreshold5 uint u0) + +;; return coinbase thresholds if contract activated +(define-read-only (get-coinbase-thresholds) + (let + ( + (activated (get-activation-status)) + ) + (asserts! activated ERR_CONTRACT_NOT_ACTIVATED) + (ok { + coinbaseThreshold1: (var-get coinbaseThreshold1), + coinbaseThreshold2: (var-get coinbaseThreshold2), + coinbaseThreshold3: (var-get coinbaseThreshold3), + coinbaseThreshold4: (var-get coinbaseThreshold4), + coinbaseThreshold5: (var-get coinbaseThreshold5) + }) + ) +) + +;; set coinbase thresholds, used during activation +(define-private (set-coinbase-thresholds) + (let + ( + (coinbaseThresholds (try! (contract-call? 'ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-token-v2 get-coinbase-thresholds))) + ) + (var-set coinbaseThreshold1 (get coinbaseThreshold1 coinbaseThresholds)) + (var-set coinbaseThreshold2 (get coinbaseThreshold2 coinbaseThresholds)) + (var-set coinbaseThreshold3 (get coinbaseThreshold3 coinbaseThresholds)) + (var-set coinbaseThreshold4 (get coinbaseThreshold4 coinbaseThresholds)) + (var-set coinbaseThreshold5 (get coinbaseThreshold5 coinbaseThresholds)) + ;; print coinbase thresholds + (print { + coinbaseThreshold1: (var-get coinbaseThreshold1), + coinbaseThreshold2: (var-get coinbaseThreshold2), + coinbaseThreshold3: (var-get coinbaseThreshold3), + coinbaseThreshold4: (var-get coinbaseThreshold4), + coinbaseThreshold5: (var-get coinbaseThreshold5) + }) + (ok true) + ) +) + +;; guarded function for auth to update coinbase thresholds +(define-public (update-coinbase-thresholds) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (try! (set-coinbase-thresholds)) + (ok true) + ) +) + +;; coinbase rewards per threshold, used to determine rewards +(define-data-var coinbaseAmountBonus uint u0) +(define-data-var coinbaseAmount1 uint u0) +(define-data-var coinbaseAmount2 uint u0) +(define-data-var coinbaseAmount3 uint u0) +(define-data-var coinbaseAmount4 uint u0) +(define-data-var coinbaseAmount5 uint u0) +(define-data-var coinbaseAmountDefault uint u0) + +;; return coinbase amounts if contract activated +(define-read-only (get-coinbase-amounts) + (let + ( + (activated (get-activation-status)) + ) + (asserts! activated ERR_CONTRACT_NOT_ACTIVATED) + (ok { + coinbaseAmountBonus: (var-get coinbaseAmountBonus), + coinbaseAmount1: (var-get coinbaseAmount1), + coinbaseAmount2: (var-get coinbaseAmount2), + coinbaseAmount3: (var-get coinbaseAmount3), + coinbaseAmount4: (var-get coinbaseAmount4), + coinbaseAmount5: (var-get coinbaseAmount5), + coinbaseAmountDefault: (var-get coinbaseAmountDefault) + }) + ) +) + +;; set coinbase amounts, used during activation +(define-private (set-coinbase-amounts) + (let + ( + (coinbaseAmounts (unwrap! (contract-call? 'ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-token-v2 get-coinbase-amounts) ERR_COINBASE_AMOUNTS_NOT_FOUND)) + ) + (var-set coinbaseAmountBonus (get coinbaseAmountBonus coinbaseAmounts)) + (var-set coinbaseAmount1 (get coinbaseAmount1 coinbaseAmounts)) + (var-set coinbaseAmount2 (get coinbaseAmount2 coinbaseAmounts)) + (var-set coinbaseAmount3 (get coinbaseAmount3 coinbaseAmounts)) + (var-set coinbaseAmount4 (get coinbaseAmount4 coinbaseAmounts)) + (var-set coinbaseAmount5 (get coinbaseAmount5 coinbaseAmounts)) + (var-set coinbaseAmountDefault (get coinbaseAmountDefault coinbaseAmounts)) + ;; print coinbase amounts + (print { + coinbaseAmountBonus: (var-get coinbaseAmountBonus), + coinbaseAmount1: (var-get coinbaseAmount1), + coinbaseAmount2: (var-get coinbaseAmount2), + coinbaseAmount3: (var-get coinbaseAmount3), + coinbaseAmount4: (var-get coinbaseAmount4), + coinbaseAmount5: (var-get coinbaseAmount5), + coinbaseAmountDefault: (var-get coinbaseAmountDefault) + }) + (ok true) + ) +) + +;; guarded function for auth to update coinbase amounts +(define-public (update-coinbase-amounts) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (try! (set-coinbase-amounts)) + (ok true) + ) +) + +;; function for deciding how many tokens to mint, depending on when they were mined +(define-read-only (get-coinbase-amount (minerBlockHeight uint)) + (begin + ;; if contract is not active, return 0 + (asserts! (>= minerBlockHeight (var-get activationBlock)) u0) + ;; if contract is active, return based on emissions schedule + ;; defined in CCIP-008 https://github.com/citycoins/governance + (asserts! (> minerBlockHeight (var-get coinbaseThreshold1)) + (if (<= (- minerBlockHeight (var-get activationBlock)) TOKEN_BONUS_PERIOD) + ;; bonus reward for initial miners + (var-get coinbaseAmountBonus) + ;; standard reward until 1st halving + (var-get coinbaseAmount1) + ) + ) + ;; computations based on each halving threshold + (asserts! (> minerBlockHeight (var-get coinbaseThreshold2)) (var-get coinbaseAmount2)) + (asserts! (> minerBlockHeight (var-get coinbaseThreshold3)) (var-get coinbaseAmount3)) + (asserts! (> minerBlockHeight (var-get coinbaseThreshold4)) (var-get coinbaseAmount4)) + (asserts! (> minerBlockHeight (var-get coinbaseThreshold5)) (var-get coinbaseAmount5)) + ;; default value after 5th halving + (var-get coinbaseAmountDefault) + ) +) + +;; mint new tokens for claimant who won at given Stacks block height +(define-private (mint-coinbase (recipient principal) (stacksHeight uint)) + (as-contract (contract-call? 'ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-token-v2 mint (get-coinbase-amount stacksHeight) recipient)) +) + +;; UTILITIES + +(define-data-var shutdownHeight uint u0) +(define-data-var isShutdown bool false) + +;; stop mining and stacking operations +;; in preparation for a core upgrade +(define-public (shutdown-contract (stacksHeight uint)) + (begin + ;; make sure block height is in the future + (asserts! (>= stacksHeight block-height) ERR_BLOCK_HEIGHT_IN_PAST) + ;; only allow shutdown request from AUTH + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + ;; set variables to disable mining/stacking in CORE + (var-set activationReached false) + (var-set shutdownHeight stacksHeight) + ;; set variable to allow for all stacking claims + (var-set isShutdown true) + (ok true) + ) +) + +;; checks if caller is Auth contract +(define-private (is-authorized-auth) + (is-eq contract-caller 'ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-auth-v2) +) + +;; checks if contract is fully activated to +;; enable mining and stacking functions +(define-private (is-activated) + (and (get-activation-status) (>= block-height (var-get activationTarget))) +) diff --git a/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-core-v2.json b/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-core-v2.json new file mode 100644 index 00000000..89c8a70a --- /dev/null +++ b/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-core-v2.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-token-v2.clar b/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-token-v2.clar new file mode 100644 index 00000000..60cd5610 --- /dev/null +++ b/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-token-v2.clar @@ -0,0 +1,281 @@ +;; MIAMICOIN TOKEN V2 CONTRACT TESTNET +;; CityCoins Protocol Version 2.0.0 + +;; TRAIT DEFINITIONS + +(impl-trait 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-token-v2-trait.citycoin-token-v2) + +;; ERROR CODES + +(define-constant ERR_UNAUTHORIZED (err u2000)) +(define-constant ERR_TOKEN_NOT_ACTIVATED (err u2001)) +(define-constant ERR_TOKEN_ALREADY_ACTIVATED (err u2002)) +(define-constant ERR_V1_BALANCE_NOT_FOUND (err u2003)) +(define-constant ERR_INVALID_COINBASE_THRESHOLD (err u2004)) +(define-constant ERR_INVALID_COINBASE_AMOUNT (err u2005)) + +;; SIP-010 DEFINITION + +(impl-trait 'ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.sip-010-trait-ft-standard.sip-010-trait) + +(define-fungible-token miamicoin) + +(define-constant DECIMALS u6) +(define-constant MICRO_CITYCOINS (pow u10 DECIMALS)) + +;; SIP-010 FUNCTIONS + +(define-public (transfer (amount uint) (from principal) (to principal) (memo (optional (buff 34)))) + (begin + (asserts! (is-eq from tx-sender) ERR_UNAUTHORIZED) + (if (is-some memo) + (print memo) + none + ) + (ft-transfer? miamicoin amount from to) + ) +) + +(define-read-only (get-name) + (ok "miamicoin") +) + +(define-read-only (get-symbol) + (ok "MIA") +) + +(define-read-only (get-decimals) + (ok DECIMALS) +) + +(define-read-only (get-balance (user principal)) + (ok (ft-get-balance miamicoin user)) +) + +(define-read-only (get-total-supply) + (ok (ft-get-supply miamicoin)) +) + +(define-read-only (get-token-uri) + (ok (var-get tokenUri)) +) + +;; TOKEN CONFIGURATION + +;; define bonus period and initial epoch length +(define-constant TOKEN_BONUS_PERIOD u10000) +(define-constant TOKEN_EPOCH_LENGTH u25000) + +;; once activated, activation cannot happen again +(define-data-var tokenActivated bool false) + +;; core contract states +(define-constant STATE_DEPLOYED u0) +(define-constant STATE_ACTIVE u1) +(define-constant STATE_INACTIVE u2) + +;; one-time function to activate the token +(define-public (activate-token (coreContract principal) (stacksHeight uint)) + (let + ( + (coreContractMap (try! (contract-call? 'ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-auth-v2 get-core-contract-info coreContract))) + (threshold1 (+ stacksHeight TOKEN_BONUS_PERIOD TOKEN_EPOCH_LENGTH)) ;; 35,000 blocks + (threshold2 (+ stacksHeight TOKEN_BONUS_PERIOD (* u3 TOKEN_EPOCH_LENGTH))) ;; 85,000 blocks + (threshold3 (+ stacksHeight TOKEN_BONUS_PERIOD (* u7 TOKEN_EPOCH_LENGTH))) ;; 185,000 blocks + (threshold4 (+ stacksHeight TOKEN_BONUS_PERIOD (* u15 TOKEN_EPOCH_LENGTH))) ;; 385,000 blocks + (threshold5 (+ stacksHeight TOKEN_BONUS_PERIOD (* u31 TOKEN_EPOCH_LENGTH))) ;; 785,000 blocks + ) + (asserts! (is-eq (get state coreContractMap) STATE_ACTIVE) ERR_UNAUTHORIZED) + (asserts! (not (var-get tokenActivated)) ERR_TOKEN_ALREADY_ACTIVATED) + (var-set tokenActivated true) + (try! (set-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5)) + (ok true) + ) +) + +;; COINBASE THRESHOLDS + +;; coinbase thresholds per halving, used to select coinbase rewards in core +;; initially set by register-user in core contract per CCIP-008 +(define-data-var coinbaseThreshold1 uint u0) +(define-data-var coinbaseThreshold2 uint u0) +(define-data-var coinbaseThreshold3 uint u0) +(define-data-var coinbaseThreshold4 uint u0) +(define-data-var coinbaseThreshold5 uint u0) + +;; return coinbase thresholds if token activated +(define-read-only (get-coinbase-thresholds) + (let + ( + (activated (var-get tokenActivated)) + ) + (asserts! activated ERR_TOKEN_NOT_ACTIVATED) + (ok { + coinbaseThreshold1: (var-get coinbaseThreshold1), + coinbaseThreshold2: (var-get coinbaseThreshold2), + coinbaseThreshold3: (var-get coinbaseThreshold3), + coinbaseThreshold4: (var-get coinbaseThreshold4), + coinbaseThreshold5: (var-get coinbaseThreshold5) + }) + ) +) + +(define-private (set-coinbase-thresholds (threshold1 uint) (threshold2 uint) (threshold3 uint) (threshold4 uint) (threshold5 uint)) + (begin + ;; check that all thresholds increase in value + (asserts! (and (> threshold1 u0) (> threshold2 threshold1) (> threshold3 threshold2) (> threshold4 threshold3) (> threshold5 threshold4)) ERR_INVALID_COINBASE_THRESHOLD) + ;; set coinbase thresholds + (var-set coinbaseThreshold1 threshold1) + (var-set coinbaseThreshold2 threshold2) + (var-set coinbaseThreshold3 threshold3) + (var-set coinbaseThreshold4 threshold4) + (var-set coinbaseThreshold5 threshold5) + ;; print coinbase thresholds + (print { + coinbaseThreshold1: threshold1, + coinbaseThreshold2: threshold2, + coinbaseThreshold3: threshold3, + coinbaseThreshold4: threshold4, + coinbaseThreshold5: threshold5 + }) + (ok true) + ) +) + +;; only accessible by auth +(define-public (update-coinbase-thresholds (threshold1 uint) (threshold2 uint) (threshold3 uint) (threshold4 uint) (threshold5 uint)) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (asserts! (var-get tokenActivated) ERR_TOKEN_NOT_ACTIVATED) + (try! (set-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5)) + (ok true) + ) +) + +;; COINBASE AMOUNTS (REWARDS) + +;; coinbase rewards per threshold per CCIP-008 +(define-data-var coinbaseAmountBonus uint (* MICRO_CITYCOINS u250000)) +(define-data-var coinbaseAmount1 uint (* MICRO_CITYCOINS u100000)) +(define-data-var coinbaseAmount2 uint (* MICRO_CITYCOINS u50000)) +(define-data-var coinbaseAmount3 uint (* MICRO_CITYCOINS u25000)) +(define-data-var coinbaseAmount4 uint (* MICRO_CITYCOINS u12500)) +(define-data-var coinbaseAmount5 uint (* MICRO_CITYCOINS u6250)) +(define-data-var coinbaseAmountDefault uint (* MICRO_CITYCOINS u3125)) + +;; return coinbase thresholds if token activated +(define-read-only (get-coinbase-amounts) + (ok { + coinbaseAmountBonus: (var-get coinbaseAmountBonus), + coinbaseAmount1: (var-get coinbaseAmount1), + coinbaseAmount2: (var-get coinbaseAmount2), + coinbaseAmount3: (var-get coinbaseAmount3), + coinbaseAmount4: (var-get coinbaseAmount4), + coinbaseAmount5: (var-get coinbaseAmount5), + coinbaseAmountDefault: (var-get coinbaseAmountDefault) + }) +) + +(define-private (set-coinbase-amounts (amountBonus uint) (amount1 uint) (amount2 uint) (amount3 uint) (amount4 uint) (amount5 uint) (amountDefault uint)) + (begin + ;; check that all amounts are greater than zero + (asserts! (and (> amountBonus u0) (> amount1 u0) (> amount2 u0) (> amount3 u0) (> amount4 u0) (> amount5 u0) (> amountDefault u0)) ERR_INVALID_COINBASE_AMOUNT) + ;; set coinbase amounts in token contract + (var-set coinbaseAmountBonus amountBonus) + (var-set coinbaseAmount1 amount1) + (var-set coinbaseAmount2 amount2) + (var-set coinbaseAmount3 amount3) + (var-set coinbaseAmount4 amount4) + (var-set coinbaseAmount5 amount5) + (var-set coinbaseAmountDefault amountDefault) + ;; print coinbase amounts + (print { + coinbaseAmountBonus: amountBonus, + coinbaseAmount1: amount1, + coinbaseAmount2: amount2, + coinbaseAmount3: amount3, + coinbaseAmount4: amount4, + coinbaseAmount5: amount5, + coinbaseAmountDefault: amountDefault + }) + (ok true) + ) +) + +;; only accessible by auth +(define-public (update-coinbase-amounts (amountBonus uint) (amount1 uint) (amount2 uint) (amount3 uint) (amount4 uint) (amount5 uint) (amountDefault uint)) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + ;; (asserts! (var-get tokenActivated) ERR_TOKEN_NOT_ACTIVATED) + (try! (set-coinbase-amounts amountBonus amount1 amount2 amount3 amount4 amount5 amountDefault)) + (ok true) + ) +) + +;; V1 TO V2 CONVERSION + +;; TESTNET: convert-to-v2 removed, no v1 + +;; UTILITIES + +(define-data-var tokenUri (optional (string-utf8 256)) (some u"https://cdn.citycoins.co/metadata/miamicoin.json")) + +;; set token URI to new value, only accessible by Auth +(define-public (set-token-uri (newUri (optional (string-utf8 256)))) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (ok (var-set tokenUri newUri)) + ) +) + +;; mint new tokens, only accessible by a Core contract +(define-public (mint (amount uint) (recipient principal)) + (let + ( + (coreContract (try! (contract-call? 'ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-auth-v2 get-core-contract-info contract-caller))) + ) + (ft-mint? miamicoin amount recipient) + ) +) + +;; burn tokens +(define-public (burn (amount uint) (owner principal)) + (begin + (asserts! (is-eq tx-sender owner) ERR_UNAUTHORIZED) + (ft-burn? miamicoin amount owner) + ) +) + +;; checks if caller is Auth contract +(define-private (is-authorized-auth) + (is-eq contract-caller 'ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-auth-v2) +) + +;; SEND-MANY + +(define-public (send-many (recipients (list 200 { to: principal, amount: uint, memo: (optional (buff 34)) }))) + (fold check-err + (map send-token recipients) + (ok true) + ) +) + +(define-private (check-err (result (response bool uint)) (prior (response bool uint))) + (match prior ok-value + result + err-value (err err-value) + ) +) + +(define-private (send-token (recipient { to: principal, amount: uint, memo: (optional (buff 34)) })) + (send-token-with-memo (get amount recipient) (get to recipient) (get memo recipient)) +) + +(define-private (send-token-with-memo (amount uint) (to principal) (memo (optional (buff 34)))) + (let + ( + (transferOk (try! (transfer amount tx-sender to memo))) + ) + (ok transferOk) + ) +) diff --git a/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-token-v2.json b/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-token-v2.json new file mode 100644 index 00000000..89c8a70a --- /dev/null +++ b/.cache/requirements/ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-token-v2.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.nft-trait.clar b/.cache/requirements/ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.nft-trait.clar new file mode 100644 index 00000000..5dd36464 --- /dev/null +++ b/.cache/requirements/ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.nft-trait.clar @@ -0,0 +1,15 @@ +(define-trait nft-trait + ( + ;; Last token ID, limited to uint range + (get-last-token-id () (response uint uint)) + + ;; URI for metadata associated with the token + (get-token-uri (uint) (response (optional (string-ascii 256)) uint)) + + ;; Owner of a given token identifier + (get-owner (uint) (response (optional principal) uint)) + + ;; Transfer from the sender to a new principal + (transfer (uint principal principal) (response bool uint)) + ) +) \ No newline at end of file diff --git a/.cache/requirements/ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.nft-trait.json b/.cache/requirements/ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.nft-trait.json new file mode 100644 index 00000000..11162a13 --- /dev/null +++ b/.cache/requirements/ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.nft-trait.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch20", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.sip-010-trait-ft-standard.clar b/.cache/requirements/ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.sip-010-trait-ft-standard.clar new file mode 100644 index 00000000..ae299b1b --- /dev/null +++ b/.cache/requirements/ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.sip-010-trait-ft-standard.clar @@ -0,0 +1,24 @@ +(define-trait sip-010-trait + ( + ;; Transfer from the caller to a new principal + (transfer (uint principal principal (optional (buff 34))) (response bool uint)) + + ;; the human readable name of the token + (get-name () (response (string-ascii 32) uint)) + + ;; the ticker symbol, or empty if none + (get-symbol () (response (string-ascii 32) uint)) + + ;; the number of decimals used, e.g. 6 would mean 1_000_000 represents 1 token + (get-decimals () (response uint uint)) + + ;; the balance of the passed principal + (get-balance (principal) (response uint uint)) + + ;; the current total supply (which does not need to be a constant) + (get-total-supply () (response uint uint)) + + ;; an optional URI that represents metadata of this token + (get-token-uri () (response (optional (string-utf8 256)) uint)) + ) +) \ No newline at end of file diff --git a/.cache/requirements/ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.sip-010-trait-ft-standard.json b/.cache/requirements/ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.sip-010-trait-ft-standard.json new file mode 100644 index 00000000..89c8a70a --- /dev/null +++ b/.cache/requirements/ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.sip-010-trait-ft-standard.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-core-v2-trait.clar b/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-core-v2-trait.clar new file mode 100644 index 00000000..ebb65396 --- /dev/null +++ b/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-core-v2-trait.clar @@ -0,0 +1,47 @@ +;; CITYCOIN CORE TRAIT V2 + +(define-trait citycoin-core-v2 + ( + + (register-user ((optional (string-utf8 50))) + (response bool uint) + ) + + (mine-tokens (uint (optional (buff 34))) + (response bool uint) + ) + + (mine-many ((list 200 uint)) + (response bool uint) + ) + + (claim-mining-reward (uint) + (response bool uint) + ) + + (stack-tokens (uint uint) + (response bool uint) + ) + + (claim-stacking-reward (uint) + (response bool uint) + ) + + (set-city-wallet (principal) + (response bool uint) + ) + + (update-coinbase-amounts () + (response bool uint) + ) + + (update-coinbase-thresholds () + (response bool uint) + ) + + (shutdown-contract (uint) + (response bool uint) + ) + + ) +) diff --git a/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-core-v2-trait.json b/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-core-v2-trait.json new file mode 100644 index 00000000..89c8a70a --- /dev/null +++ b/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-core-v2-trait.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-token-v2-trait.clar b/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-token-v2-trait.clar new file mode 100644 index 00000000..794ebda8 --- /dev/null +++ b/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-token-v2-trait.clar @@ -0,0 +1,35 @@ +;; CITYCOIN TOKEN TRAIT + +(define-trait citycoin-token-v2 + ( + + (activate-token (principal uint) + (response bool uint) + ) + + (set-token-uri ((optional (string-utf8 256))) + (response bool uint) + ) + + (mint (uint principal) + (response bool uint) + ) + + (burn (uint principal) + (response bool uint) + ) + + (send-many ((list 200 { to: principal, amount: uint, memo: (optional (buff 34)) })) + (response bool uint) + ) + + (update-coinbase-thresholds (uint uint uint uint uint) + (response bool uint) + ) + + (update-coinbase-amounts (uint uint uint uint uint uint uint) + (response bool uint) + ) + + ) +) diff --git a/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-token-v2-trait.json b/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-token-v2-trait.json new file mode 100644 index 00000000..89c8a70a --- /dev/null +++ b/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-token-v2-trait.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-vrf-v2.clar b/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-vrf-v2.clar new file mode 100644 index 00000000..1278c676 --- /dev/null +++ b/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-vrf-v2.clar @@ -0,0 +1,69 @@ +;; CITYCOIN VRF CONTRACT V2 +;; CityCoins Protocol Version 2.0.0 + +;; ERROR CODES + +(define-constant ERR_FAIL (err u3000)) + +;; CONFIGURATION + +(define-map RandomUintAtBlock uint uint) + +;; PUBLIC FUNCTIONS + +;; returns the saved random integer +;; or, calculates and saves it to the map +;; #[allow(unchecked_data)] +(define-public (get-save-rnd (block uint)) + (match (map-get? RandomUintAtBlock block) + rnd (ok rnd) + (match (read-rnd block) + rnd (begin (map-set RandomUintAtBlock block rnd) (ok rnd)) + err-val (err err-val) + ) + ) +) + +;; returns the saved random integer +;; or, calculates and returns the value +(define-read-only (get-rnd (block uint)) + (match (map-get? RandomUintAtBlock block) + rnd (ok rnd) + (read-rnd block) + ) +) + +;; PRIVATE FUNCTIONS + +(define-private (read-rnd (block uint)) + (ok (lower-16-le (unwrap! (get-block-info? vrf-seed block) ERR_FAIL))) +) + +(define-private (lower-16-le (vrfSeed (buff 32))) + (+ + (lower-16-le-inner (element-at vrfSeed u16) u15) + (lower-16-le-inner (element-at vrfSeed u17) u14) + (lower-16-le-inner (element-at vrfSeed u18) u13) + (lower-16-le-inner (element-at vrfSeed u19) u12) + (lower-16-le-inner (element-at vrfSeed u20) u11) + (lower-16-le-inner (element-at vrfSeed u21) u10) + (lower-16-le-inner (element-at vrfSeed u22) u9) + (lower-16-le-inner (element-at vrfSeed u23) u8) + (lower-16-le-inner (element-at vrfSeed u24) u7) + (lower-16-le-inner (element-at vrfSeed u25) u6) + (lower-16-le-inner (element-at vrfSeed u26) u5) + (lower-16-le-inner (element-at vrfSeed u27) u4) + (lower-16-le-inner (element-at vrfSeed u28) u3) + (lower-16-le-inner (element-at vrfSeed u29) u2) + (lower-16-le-inner (element-at vrfSeed u30) u1) + (lower-16-le-inner (element-at vrfSeed u31) u0) + ) +) + +(define-private (lower-16-le-inner (byte (optional (buff 1))) (pos uint)) + (* (buff-to-u8 (unwrap-panic byte)) (pow u2 (* u8 pos))) +) + +(define-private (buff-to-u8 (byte (buff 1))) + (unwrap-panic (index-of 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff byte)) +) diff --git a/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-vrf-v2.json b/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-vrf-v2.json new file mode 100644 index 00000000..89c8a70a --- /dev/null +++ b/.cache/requirements/ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-vrf-v2.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-auth-v2.clar b/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-auth-v2.clar new file mode 100644 index 00000000..74a1f8b5 --- /dev/null +++ b/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-auth-v2.clar @@ -0,0 +1,630 @@ +;; NEWYORKCITYCOIN AUTH CONTRACT V2 TESTNET +;; CityCoins Protocol Version 2.0.0 + +(define-constant CONTRACT_OWNER tx-sender) + +;; TRAIT DEFINITIONS + +(use-trait coreTraitV2 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-core-v2-trait.citycoin-core-v2) +(use-trait tokenTraitV2 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-token-v2-trait.citycoin-token-v2) + +;; ERRORS + +(define-constant ERR_UNKNOWN_JOB (err u6000)) +(define-constant ERR_UNAUTHORIZED (err u6001)) +(define-constant ERR_JOB_IS_ACTIVE (err u6002)) +(define-constant ERR_JOB_IS_NOT_ACTIVE (err u6003)) +(define-constant ERR_ALREADY_VOTED_THIS_WAY (err u6004)) +(define-constant ERR_JOB_IS_EXECUTED (err u6005)) +(define-constant ERR_JOB_IS_NOT_APPROVED (err u6006)) +(define-constant ERR_ARGUMENT_ALREADY_EXISTS (err u6007)) +(define-constant ERR_NO_ACTIVE_CORE_CONTRACT (err u6008)) +(define-constant ERR_CORE_CONTRACT_NOT_FOUND (err u6009)) +(define-constant ERR_UNKNOWN_ARGUMENT (err u6010)) +(define-constant ERR_INCORRECT_CONTRACT_STATE (err u6011)) +(define-constant ERR_CONTRACT_ALREADY_EXISTS (err u6012)) + +;; JOB MANAGEMENT + +(define-constant REQUIRED_APPROVALS u2) + +(define-data-var lastJobId uint u0) + +(define-map Jobs + uint + { + creator: principal, + name: (string-ascii 255), + target: principal, + approvals: uint, + disapprovals: uint, + isActive: bool, + isExecuted: bool + } +) + +(define-map JobApprovers + { jobId: uint, approver: principal } + bool +) + +(define-map Approvers + principal + bool +) + +(define-map ArgumentLastIdsByType + { jobId: uint, argumentType: (string-ascii 25) } + uint +) + +(define-map UIntArgumentsByName + { jobId: uint, argumentName: (string-ascii 255) } + { argumentId: uint, value: uint} +) + +(define-map UIntArgumentsById + { jobId: uint, argumentId: uint } + { argumentName: (string-ascii 255), value: uint } +) + +(define-map PrincipalArgumentsByName + { jobId: uint, argumentName: (string-ascii 255) } + { argumentId: uint, value: principal } +) + +(define-map PrincipalArgumentsById + { jobId: uint, argumentId: uint } + { argumentName: (string-ascii 255), value: principal } +) + +;; FUNCTIONS + +(define-read-only (get-last-job-id) + (var-get lastJobId) +) + +(define-public (create-job (name (string-ascii 255)) (target principal)) + (let + ( + (newJobId (+ (var-get lastJobId) u1)) + ) + (asserts! (is-approver tx-sender) ERR_UNAUTHORIZED) + (map-set Jobs + newJobId + { + creator: tx-sender, + name: name, + target: target, + approvals: u0, + disapprovals: u0, + isActive: false, + isExecuted: false + } + ) + (var-set lastJobId newJobId) + (ok newJobId) + ) +) + +(define-read-only (get-job (jobId uint)) + (map-get? Jobs jobId) +) + +(define-public (activate-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + ) + (asserts! (is-eq (get creator job) tx-sender) ERR_UNAUTHORIZED) + (asserts! (not (get isActive job)) ERR_JOB_IS_ACTIVE) + (map-set Jobs + jobId + (merge job { isActive: true }) + ) + (ok true) + ) +) + +(define-public (approve-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + (previousVote (map-get? JobApprovers { jobId: jobId, approver: tx-sender })) + ) + (asserts! (get isActive job) ERR_JOB_IS_NOT_ACTIVE) + (asserts! (is-approver tx-sender) ERR_UNAUTHORIZED) + ;; save vote + (map-set JobApprovers + { jobId: jobId, approver: tx-sender } + true + ) + (match previousVote approved + (begin + (asserts! (not approved) ERR_ALREADY_VOTED_THIS_WAY) + (map-set Jobs jobId + (merge job + { + approvals: (+ (get approvals job) u1), + disapprovals: (- (get disapprovals job) u1) + } + ) + ) + ) + ;; no previous vote + (map-set Jobs + jobId + (merge job { approvals: (+ (get approvals job) u1) } ) + ) + ) + (ok true) + ) +) + +(define-public (disapprove-job (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + (previousVote (map-get? JobApprovers { jobId: jobId, approver: tx-sender })) + ) + (asserts! (get isActive job) ERR_JOB_IS_NOT_ACTIVE) + (asserts! (is-approver tx-sender) ERR_UNAUTHORIZED) + ;; save vote + (map-set JobApprovers + { jobId: jobId, approver: tx-sender } + false + ) + (match previousVote approved + (begin + (asserts! approved ERR_ALREADY_VOTED_THIS_WAY) + (map-set Jobs jobId + (merge job + { + approvals: (- (get approvals job) u1), + disapprovals: (+ (get disapprovals job) u1) + } + ) + ) + ) + ;; no previous vote + (map-set Jobs + jobId + (merge job { disapprovals: (+ (get disapprovals job) u1) } ) + ) + ) + (ok true) + ) +) + +(define-read-only (is-job-approved (jobId uint)) + (match (get-job jobId) job + (>= (get approvals job) REQUIRED_APPROVALS) + false + ) +) + +(define-public (mark-job-as-executed (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + ) + (asserts! (get isActive job) ERR_JOB_IS_NOT_ACTIVE) + (asserts! (>= (get approvals job) REQUIRED_APPROVALS) ERR_JOB_IS_NOT_APPROVED) + (asserts! (is-eq (get target job) contract-caller) ERR_UNAUTHORIZED) + (asserts! (not (get isExecuted job)) ERR_JOB_IS_EXECUTED) + (map-set Jobs + jobId + (merge job { isExecuted: true }) + ) + (ok true) + ) +) + +(define-public (add-uint-argument (jobId uint) (argumentName (string-ascii 255)) (value uint)) + (let + ( + (argumentId (generate-argument-id jobId "uint")) + ) + (try! (guard-add-argument jobId)) + (asserts! + (and + (map-insert UIntArgumentsById + { jobId: jobId, argumentId: argumentId } + { argumentName: argumentName, value: value } + ) + (map-insert UIntArgumentsByName + { jobId: jobId, argumentName: argumentName } + { argumentId: argumentId, value: value} + ) + ) + ERR_ARGUMENT_ALREADY_EXISTS + ) + (ok true) + ) +) + +(define-read-only (get-uint-argument-by-name (jobId uint) (argumentName (string-ascii 255))) + (map-get? UIntArgumentsByName { jobId: jobId, argumentName: argumentName }) +) + +(define-read-only (get-uint-argument-by-id (jobId uint) (argumentId uint)) + (map-get? UIntArgumentsById { jobId: jobId, argumentId: argumentId }) +) + +(define-read-only (get-uint-value-by-name (jobId uint) (argumentName (string-ascii 255))) + (get value (get-uint-argument-by-name jobId argumentName)) +) + +(define-read-only (get-uint-value-by-id (jobId uint) (argumentId uint)) + (get value (get-uint-argument-by-id jobId argumentId)) +) + +(define-public (add-principal-argument (jobId uint) (argumentName (string-ascii 255)) (value principal)) + (let + ( + (argumentId (generate-argument-id jobId "principal")) + ) + (try! (guard-add-argument jobId)) + (asserts! + (and + (map-insert PrincipalArgumentsById + { jobId: jobId, argumentId: argumentId } + { argumentName: argumentName, value: value } + ) + (map-insert PrincipalArgumentsByName + { jobId: jobId, argumentName: argumentName } + { argumentId: argumentId, value: value} + ) + ) + ERR_ARGUMENT_ALREADY_EXISTS + ) + (ok true) + ) +) + +(define-read-only (get-principal-argument-by-name (jobId uint) (argumentName (string-ascii 255))) + (map-get? PrincipalArgumentsByName { jobId: jobId, argumentName: argumentName }) +) + +(define-read-only (get-principal-argument-by-id (jobId uint) (argumentId uint)) + (map-get? PrincipalArgumentsById { jobId: jobId, argumentId: argumentId }) +) + +(define-read-only (get-principal-value-by-name (jobId uint) (argumentName (string-ascii 255))) + (get value (get-principal-argument-by-name jobId argumentName)) +) + +(define-read-only (get-principal-value-by-id (jobId uint) (argumentId uint)) + (get value (get-principal-argument-by-id jobId argumentId)) +) + +;; PRIVATE FUNCTIONS + +(define-read-only (is-approver (user principal)) + (default-to false (map-get? Approvers user)) +) + +(define-private (generate-argument-id (jobId uint) (argumentType (string-ascii 25))) + (let + ( + (argumentId (+ (default-to u0 (map-get? ArgumentLastIdsByType { jobId: jobId, argumentType: argumentType })) u1)) + ) + (map-set ArgumentLastIdsByType + { jobId: jobId, argumentType: argumentType } + argumentId + ) + ;; return + argumentId + ) +) + +(define-private (guard-add-argument (jobId uint)) + (let + ( + (job (unwrap! (get-job jobId) ERR_UNKNOWN_JOB)) + ) + (asserts! (not (get isActive job)) ERR_JOB_IS_ACTIVE) + (asserts! (is-eq (get creator job) contract-caller) ERR_UNAUTHORIZED) + (ok true) + ) +) + +;; CONTRACT MANAGEMENT + +;; initial value for active core contract +;; set to deployer address at startup to prevent +;; circular dependency of core on auth +(define-data-var activeCoreContract principal CONTRACT_OWNER) +(define-data-var initialized bool false) + +;; core contract states +(define-constant STATE_DEPLOYED u0) +(define-constant STATE_ACTIVE u1) +(define-constant STATE_INACTIVE u2) + +;; core contract map +(define-map CoreContracts + principal + { + state: uint, + startHeight: uint, + endHeight: uint + } +) + +;; getter for active core contract +(define-read-only (get-active-core-contract) + (begin + (asserts! (not (is-eq (var-get activeCoreContract) CONTRACT_OWNER)) ERR_NO_ACTIVE_CORE_CONTRACT) + (ok (var-get activeCoreContract)) + ) +) + +;; getter for core contract map +(define-read-only (get-core-contract-info (targetContract principal)) + (let + ( + (coreContract (unwrap! (map-get? CoreContracts targetContract) ERR_CORE_CONTRACT_NOT_FOUND)) + ) + (ok coreContract) + ) +) + +;; one-time function to initialize contracts after all contracts are deployed +;; - check that deployer is calling this function +;; - check this contract is not activated already (one-time use) +;; - set initial map value for core contract v1 +;; - set cityWallet in core contract +;; - set intialized true +(define-public (initialize-contracts (coreContract )) + (let + ( + (coreContractAddress (contract-of coreContract)) + ) + (asserts! (is-eq contract-caller CONTRACT_OWNER) ERR_UNAUTHORIZED) + (asserts! (not (var-get initialized)) ERR_UNAUTHORIZED) + (map-set CoreContracts + coreContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (try! (contract-call? coreContract set-city-wallet (var-get cityWallet))) + (var-set initialized true) + (ok true) + ) +) + +(define-read-only (is-initialized) + (var-get initialized) +) + +;; function to activate core contract through registration +;; - check that target is in core contract map +;; - check that caller is core contract +;; - check that target is in STATE_DEPLOYED +;; - set active in core contract map +;; - set as activeCoreContract +(define-public (activate-core-contract (targetContract principal) (stacksHeight uint)) + (let + ( + (coreContract (unwrap! (map-get? CoreContracts targetContract) ERR_CORE_CONTRACT_NOT_FOUND)) + ) + (asserts! (is-eq (get state coreContract) STATE_DEPLOYED) ERR_INCORRECT_CONTRACT_STATE) + (asserts! (is-eq contract-caller targetContract) ERR_UNAUTHORIZED) + (map-set CoreContracts + targetContract + { + state: STATE_ACTIVE, + startHeight: stacksHeight, + endHeight: u0 + }) + (var-set activeCoreContract targetContract) + (ok true) + ) +) + +;; protected function to update core contract +(define-public (upgrade-core-contract (oldContract ) (newContract )) + (let + ( + (oldContractAddress (contract-of oldContract)) + (oldContractMap (unwrap! (map-get? CoreContracts oldContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + (newContractAddress (contract-of newContract)) + ) + (asserts! (not (is-eq oldContractAddress newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (is-none (map-get? CoreContracts newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + (map-set CoreContracts + oldContractAddress + { + state: STATE_INACTIVE, + startHeight: (get startHeight oldContractMap), + endHeight: block-height + }) + (map-set CoreContracts + newContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (var-set activeCoreContract newContractAddress) + (try! (contract-call? oldContract shutdown-contract block-height)) + (try! (contract-call? newContract set-city-wallet (var-get cityWallet))) + (ok true) + ) +) + +(define-public (execute-upgrade-core-contract-job (jobId uint) (oldContract ) (newContract )) + (let + ( + (oldContractArg (unwrap! (get-principal-value-by-name jobId "oldContract") ERR_UNKNOWN_ARGUMENT)) + (newContractArg (unwrap! (get-principal-value-by-name jobId "newContract") ERR_UNKNOWN_ARGUMENT)) + (oldContractAddress (contract-of oldContract)) + (oldContractMap (unwrap! (map-get? CoreContracts oldContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + (newContractAddress (contract-of newContract)) + ) + (asserts! (not (is-eq oldContractAddress newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (is-none (map-get? CoreContracts newContractAddress)) ERR_CONTRACT_ALREADY_EXISTS) + (asserts! (and (is-eq oldContractArg oldContractAddress) (is-eq newContractArg newContractAddress)) ERR_UNAUTHORIZED) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (map-set CoreContracts + oldContractAddress + { + state: STATE_INACTIVE, + startHeight: (get startHeight oldContractMap), + endHeight: block-height + }) + (map-set CoreContracts + newContractAddress + { + state: STATE_DEPLOYED, + startHeight: u0, + endHeight: u0 + }) + (var-set activeCoreContract newContractAddress) + (try! (contract-call? oldContract shutdown-contract block-height)) + (try! (contract-call? newContract set-city-wallet (var-get cityWallet))) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; CITY WALLET MANAGEMENT + +;; initial value for city wallet +(define-data-var cityWallet principal 'ST7G6VDV48CXXSP6J2B4RRCKTFJ5NK3PBZSD3YW5) + +;; returns city wallet principal +(define-read-only (get-city-wallet) + (ok (var-get cityWallet)) +) + +;; protected function to update city wallet variable +(define-public (set-city-wallet (targetContract ) (newCityWallet principal)) + (let + ( + (coreContractAddress (contract-of targetContract)) + (coreContract (unwrap! (map-get? CoreContracts coreContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + ) + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + (asserts! (is-eq coreContractAddress (var-get activeCoreContract)) ERR_UNAUTHORIZED) + (var-set cityWallet newCityWallet) + (try! (contract-call? targetContract set-city-wallet newCityWallet)) + (ok true) + ) +) + +(define-public (execute-set-city-wallet-job (jobId uint) (targetContract )) + (let + ( + (coreContractAddress (contract-of targetContract)) + (coreContract (unwrap! (map-get? CoreContracts coreContractAddress) ERR_CORE_CONTRACT_NOT_FOUND)) + (newCityWallet (unwrap! (get-principal-value-by-name jobId "newCityWallet") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (asserts! (is-eq coreContractAddress (var-get activeCoreContract)) ERR_UNAUTHORIZED) + (var-set cityWallet newCityWallet) + (try! (contract-call? targetContract set-city-wallet newCityWallet)) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; check if contract caller is city wallet +(define-private (is-authorized-city) + (is-eq contract-caller (var-get cityWallet)) +) + +;; TOKEN MANAGEMENT + +(define-public (set-token-uri (targetContract ) (newUri (optional (string-utf8 256)))) + (begin + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + (as-contract (try! (contract-call? targetContract set-token-uri newUri))) + (ok true) + ) +) + +;; COINBASE THRESHOLDS + +(define-public (update-coinbase-thresholds (targetCore ) (targetToken ) (threshold1 uint) (threshold2 uint) (threshold3 uint) (threshold4 uint) (threshold5 uint)) + (begin + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + ;; update in token contract + (as-contract (try! (contract-call? targetToken update-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5))) + ;; update core contract based on token contract + (as-contract (try! (contract-call? targetCore update-coinbase-thresholds))) + (ok true) + ) +) + +(define-public (execute-update-coinbase-thresholds-job (jobId uint) (targetCore ) (targetToken )) + (let + ( + (threshold1 (unwrap! (get-uint-value-by-name jobId "threshold1") ERR_UNKNOWN_ARGUMENT)) + (threshold2 (unwrap! (get-uint-value-by-name jobId "threshold2") ERR_UNKNOWN_ARGUMENT)) + (threshold3 (unwrap! (get-uint-value-by-name jobId "threshold3") ERR_UNKNOWN_ARGUMENT)) + (threshold4 (unwrap! (get-uint-value-by-name jobId "threshold4") ERR_UNKNOWN_ARGUMENT)) + (threshold5 (unwrap! (get-uint-value-by-name jobId "threshold5") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (as-contract (try! (contract-call? targetToken update-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5))) + (as-contract (try! (contract-call? targetCore update-coinbase-thresholds))) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; COINBASE AMOUNTS (REWARDS) + +(define-public (update-coinbase-amounts (targetCore ) (targetToken ) (amountBonus uint) (amount1 uint) (amount2 uint) (amount3 uint) (amount4 uint) (amount5 uint) (amountDefault uint)) + (begin + (asserts! (is-authorized-city) ERR_UNAUTHORIZED) + ;; update in token contract + (as-contract (try! (contract-call? targetToken update-coinbase-amounts amountBonus amount1 amount2 amount3 amount4 amount5 amountDefault))) + ;; update core contract based on token contract + (as-contract (try! (contract-call? targetCore update-coinbase-amounts))) + (ok true) + ) +) + +(define-public (execute-update-coinbase-amounts-job (jobId uint) (targetCore ) (targetToken )) + (let + ( + (amountBonus (unwrap! (get-uint-value-by-name jobId "amountBonus") ERR_UNKNOWN_ARGUMENT)) + (amount1 (unwrap! (get-uint-value-by-name jobId "amount1") ERR_UNKNOWN_ARGUMENT)) + (amount2 (unwrap! (get-uint-value-by-name jobId "amount2") ERR_UNKNOWN_ARGUMENT)) + (amount3 (unwrap! (get-uint-value-by-name jobId "amount3") ERR_UNKNOWN_ARGUMENT)) + (amount4 (unwrap! (get-uint-value-by-name jobId "amount4") ERR_UNKNOWN_ARGUMENT)) + (amount5 (unwrap! (get-uint-value-by-name jobId "amount5") ERR_UNKNOWN_ARGUMENT)) + (amountDefault (unwrap! (get-uint-value-by-name jobId "amountDefault") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (as-contract (try! (contract-call? targetToken update-coinbase-amounts amountBonus amount1 amount2 amount3 amount4 amount5 amountDefault))) + (as-contract (try! (contract-call? targetCore update-coinbase-amounts))) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; APPROVERS MANAGEMENT + +(define-public (execute-replace-approver-job (jobId uint)) + (let + ( + (oldApprover (unwrap! (get-principal-value-by-name jobId "oldApprover") ERR_UNKNOWN_ARGUMENT)) + (newApprover (unwrap! (get-principal-value-by-name jobId "newApprover") ERR_UNKNOWN_ARGUMENT)) + ) + (asserts! (is-approver contract-caller) ERR_UNAUTHORIZED) + (map-set Approvers oldApprover false) + (map-set Approvers newApprover true) + (as-contract (mark-job-as-executed jobId)) + ) +) + +;; CONTRACT INITIALIZATION + +(map-insert Approvers 'ST3AY0CM7SD9183QZ4Y7S2RGBZX9GQT54MJ6XY0BN true) +(map-insert Approvers 'ST2D06VFWWTNCWHVB2FJ9KJ3EB30HFRTHB1A4BSP3 true) +(map-insert Approvers 'ST113N3MMPZRMJJRZH6JTHA5CB7TBZH1EH4C22GFV true) +(map-insert Approvers 'ST8YRW1THF2XT8E45XXCGYKZH2B70HYH71VC7737 true) +(map-insert Approvers 'STX13Q7ZJDSFVDZMQ1PWDFGT4QSBMASRMCYE4NAP true) diff --git a/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-auth-v2.json b/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-auth-v2.json new file mode 100644 index 00000000..89c8a70a --- /dev/null +++ b/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-auth-v2.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-core-v2.clar b/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-core-v2.clar new file mode 100644 index 00000000..d41a3e05 --- /dev/null +++ b/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-core-v2.clar @@ -0,0 +1,1007 @@ +;; NEWYORKCITYCOIN CORE CONTRACT V2 TESTNET +;; CityCoins Protocol Version 2.0.0 + +;; GENERAL CONFIGURATION + +(impl-trait 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-core-v2-trait.citycoin-core-v2) +(define-constant CONTRACT_OWNER tx-sender) + +;; ERROR CODES + +(define-constant ERR_UNAUTHORIZED (err u1000)) +(define-constant ERR_USER_ALREADY_REGISTERED (err u1001)) +(define-constant ERR_USER_NOT_FOUND (err u1002)) +(define-constant ERR_USER_ID_NOT_FOUND (err u1003)) +(define-constant ERR_ACTIVATION_THRESHOLD_REACHED (err u1004)) +(define-constant ERR_CONTRACT_NOT_ACTIVATED (err u1005)) +(define-constant ERR_USER_ALREADY_MINED (err u1006)) +(define-constant ERR_INSUFFICIENT_COMMITMENT (err u1007)) +(define-constant ERR_INSUFFICIENT_BALANCE (err u1008)) +(define-constant ERR_USER_DID_NOT_MINE_IN_BLOCK (err u1009)) +(define-constant ERR_CLAIMED_BEFORE_MATURITY (err u1010)) +(define-constant ERR_NO_MINERS_AT_BLOCK (err u1011)) +(define-constant ERR_REWARD_ALREADY_CLAIMED (err u1012)) +(define-constant ERR_MINER_DID_NOT_WIN (err u1013)) +(define-constant ERR_NO_VRF_SEED_FOUND (err u1014)) +(define-constant ERR_STACKING_NOT_AVAILABLE (err u1015)) +(define-constant ERR_CANNOT_STACK (err u1016)) +(define-constant ERR_REWARD_CYCLE_NOT_COMPLETED (err u1017)) +(define-constant ERR_NOTHING_TO_REDEEM (err u1018)) +(define-constant ERR_UNABLE_TO_FIND_CITY_WALLET (err u1019)) +(define-constant ERR_CLAIM_IN_WRONG_CONTRACT (err u1020)) +(define-constant ERR_BLOCK_HEIGHT_IN_PAST (err u1021)) +(define-constant ERR_COINBASE_AMOUNTS_NOT_FOUND (err u1022)) + +;; CITY WALLET MANAGEMENT + +;; initial value for city wallet, set to this contract until initialized +(define-data-var cityWallet principal 'STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-core-v2) + +;; returns set city wallet principal +(define-read-only (get-city-wallet) + (var-get cityWallet) +) + +;; protected function to update city wallet variable +(define-public (set-city-wallet (newCityWallet principal)) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (ok (var-set cityWallet newCityWallet)) + ) +) + +;; REGISTRATION + +(define-constant NEWYORKCITYCOIN_ACTIVATION_HEIGHT u37449) +(define-data-var activationBlock uint u340282366920938463463374607431768211455) +(define-data-var activationDelay uint u1) ;; TESTNET: set to 1 block +(define-data-var activationReached bool false) +(define-data-var activationTarget uint u0) +(define-data-var activationThreshold uint u2) ;; TESTNET: set to 2 users +(define-data-var usersNonce uint u0) + +;; returns Stacks block height registration was activated at plus activationDelay +(define-read-only (get-activation-block) + (begin + (asserts! (get-activation-status) ERR_CONTRACT_NOT_ACTIVATED) + (ok (var-get activationBlock)) + ) +) + +;; returns activation delay +(define-read-only (get-activation-delay) + (var-get activationDelay) +) + +;; returns activation status as boolean +(define-read-only (get-activation-status) + (var-get activationReached) +) + +;; returns activation target +(define-read-only (get-activation-target) + (begin + (asserts! (get-activation-status) ERR_CONTRACT_NOT_ACTIVATED) + (ok (var-get activationTarget)) + ) +) + +;; returns activation threshold +(define-read-only (get-activation-threshold) + (var-get activationThreshold) +) + +;; returns number of registered users, used for activation and tracking user IDs +(define-read-only (get-registered-users-nonce) + (var-get usersNonce) +) + +;; store user principal by user id +(define-map Users + uint + principal +) + +;; store user id by user principal +(define-map UserIds + principal + uint +) + +;; returns (some userId) or none +(define-read-only (get-user-id (user principal)) + (map-get? UserIds user) +) + +;; returns (some userPrincipal) or none +(define-read-only (get-user (userId uint)) + (map-get? Users userId) +) + +;; returns user ID if it has been created, or creates and returns new ID +(define-private (get-or-create-user-id (user principal)) + (match + (map-get? UserIds user) + value value + (let + ( + (newId (+ u1 (var-get usersNonce))) + ) + (map-set Users newId user) + (map-set UserIds user newId) + (var-set usersNonce newId) + newId + ) + ) +) + +;; registers users that signal activation of contract until threshold is met +(define-public (register-user (memo (optional (string-utf8 50)))) + (let + ( + (newId (+ u1 (var-get usersNonce))) + (threshold (var-get activationThreshold)) + (initialized (contract-call? 'STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-auth-v2 is-initialized)) + ) + + (asserts! initialized ERR_UNAUTHORIZED) + + (asserts! (is-none (map-get? UserIds tx-sender)) + ERR_USER_ALREADY_REGISTERED) + + (asserts! (<= newId threshold) + ERR_ACTIVATION_THRESHOLD_REACHED) + + (if (is-some memo) + (print memo) + none + ) + + (get-or-create-user-id tx-sender) + + (if (is-eq newId threshold) + (let + ( + (activationTargetBlock (+ block-height (var-get activationDelay))) + ) + (try! (contract-call? 'STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-auth-v2 activate-core-contract (as-contract tx-sender) activationTargetBlock)) + (try! (contract-call? 'STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-token-v2 activate-token (as-contract tx-sender) NEWYORKCITYCOIN_ACTIVATION_HEIGHT)) + (try! (set-coinbase-thresholds)) + (try! (set-coinbase-amounts)) + (var-set activationReached true) + (var-set activationBlock NEWYORKCITYCOIN_ACTIVATION_HEIGHT) + (var-set activationTarget activationTargetBlock) + (ok true) + ) + (ok true) + ) + ) +) + +;; MINING CONFIGURATION + +;; define split to custodied wallet address for the city +(define-constant SPLIT_CITY_PCT u30) + +;; how long a miner must wait before block winner can claim their minted tokens +(define-data-var tokenRewardMaturity uint u10) ;; TESTNET: set to 10 blocks + +;; At a given Stacks block height: +;; - how many miners were there +;; - what was the total amount submitted +;; - what was the total amount submitted to the city +;; - what was the total amount submitted to Stackers +;; - was the block reward claimed +(define-map MiningStatsAtBlock + uint + { + minersCount: uint, + amount: uint, + amountToCity: uint, + amountToStackers: uint, + rewardClaimed: bool + } +) + +;; returns map MiningStatsAtBlock at a given Stacks block height if it exists +(define-read-only (get-mining-stats-at-block (stacksHeight uint)) + (map-get? MiningStatsAtBlock stacksHeight) +) + +;; returns map MiningStatsAtBlock at a given Stacks block height +;; or, an empty structure +(define-read-only (get-mining-stats-at-block-or-default (stacksHeight uint)) + (default-to { + minersCount: u0, + amount: u0, + amountToCity: u0, + amountToStackers: u0, + rewardClaimed: false + } + (map-get? MiningStatsAtBlock stacksHeight) + ) +) + +;; At a given Stacks block height and user ID: +;; - what is their ustx commitment +;; - what are the low/high values (used for VRF) +(define-map MinersAtBlock + { + stacksHeight: uint, + userId: uint + } + { + ustx: uint, + lowValue: uint, + highValue: uint, + winner: bool + } +) + +;; returns true if a given miner has already mined at a given block height +(define-read-only (has-mined-at-block (stacksHeight uint) (userId uint)) + (is-some + (map-get? MinersAtBlock { stacksHeight: stacksHeight, userId: userId }) + ) +) + +;; returns map MinersAtBlock at a given Stacks block height for a user ID +(define-read-only (get-miner-at-block (stacksHeight uint) (userId uint)) + (map-get? MinersAtBlock { stacksHeight: stacksHeight, userId: userId }) +) + +;; returns map MinersAtBlock at a given Stacks block height for a user ID +;; or, an empty structure +(define-read-only (get-miner-at-block-or-default (stacksHeight uint) (userId uint)) + (default-to { + highValue: u0, + lowValue: u0, + ustx: u0, + winner: false + } + (map-get? MinersAtBlock { stacksHeight: stacksHeight, userId: userId })) +) + +;; At a given Stacks block height: +;; - what is the max highValue from MinersAtBlock (used for VRF) +(define-map MinersAtBlockHighValue + uint + uint +) + +;; returns last high value from map MinersAtBlockHighValue +(define-read-only (get-last-high-value-at-block (stacksHeight uint)) + (default-to u0 + (map-get? MinersAtBlockHighValue stacksHeight)) +) + +;; At a given Stacks block height: +;; - what is the userId of miner who won this block +(define-map BlockWinnerIds + uint + uint +) + +(define-read-only (get-block-winner-id (stacksHeight uint)) + (map-get? BlockWinnerIds stacksHeight) +) + +;; MINING ACTIONS + +(define-public (mine-tokens (amountUstx uint) (memo (optional (buff 34)))) + (let + ( + (userId (get-or-create-user-id tx-sender)) + ) + (try! (mine-tokens-at-block userId block-height amountUstx memo)) + (ok true) + ) +) + +(define-public (mine-many (amounts (list 200 uint))) + (begin + (asserts! (is-activated) ERR_CONTRACT_NOT_ACTIVATED) + (asserts! (> (len amounts) u0) ERR_INSUFFICIENT_COMMITMENT) + (match (fold mine-single amounts (ok { userId: (get-or-create-user-id tx-sender), toStackers: u0, toCity: u0, stacksHeight: block-height })) + okReturn + (begin + (asserts! (>= (stx-get-balance tx-sender) (+ (get toStackers okReturn) (get toCity okReturn))) ERR_INSUFFICIENT_BALANCE) + (if (> (get toStackers okReturn ) u0) + (try! (stx-transfer? (get toStackers okReturn ) tx-sender (as-contract tx-sender))) + false + ) + (try! (stx-transfer? (get toCity okReturn) tx-sender (var-get cityWallet))) + (print { + firstBlock: block-height, + lastBlock: (- (+ block-height (len amounts)) u1) + }) + (ok true) + ) + errReturn (err errReturn) + ) + ) +) + +(define-private (mine-single + (amountUstx uint) + (return (response + { + userId: uint, + toStackers: uint, + toCity: uint, + stacksHeight: uint + } + uint + ))) + + (match return okReturn + (let + ( + (stacksHeight (get stacksHeight okReturn)) + (rewardCycle (default-to u0 (get-reward-cycle stacksHeight))) + (stackingActive (stacking-active-at-cycle rewardCycle)) + (toCity + (if stackingActive + (/ (* SPLIT_CITY_PCT amountUstx) u100) + amountUstx + ) + ) + (toStackers (- amountUstx toCity)) + ) + (asserts! (not (has-mined-at-block stacksHeight (get userId okReturn))) ERR_USER_ALREADY_MINED) + (asserts! (> amountUstx u0) ERR_INSUFFICIENT_COMMITMENT) + (try! (set-tokens-mined (get userId okReturn) stacksHeight amountUstx toStackers toCity)) + (ok (merge okReturn + { + toStackers: (+ (get toStackers okReturn) toStackers), + toCity: (+ (get toCity okReturn) toCity), + stacksHeight: (+ stacksHeight u1) + } + )) + ) + errReturn (err errReturn) + ) +) + +(define-private (mine-tokens-at-block (userId uint) (stacksHeight uint) (amountUstx uint) (memo (optional (buff 34)))) + (let + ( + (rewardCycle (default-to u0 (get-reward-cycle stacksHeight))) + (stackingActive (stacking-active-at-cycle rewardCycle)) + (toCity + (if stackingActive + (/ (* SPLIT_CITY_PCT amountUstx) u100) + amountUstx + ) + ) + (toStackers (- amountUstx toCity)) + ) + (asserts! (is-activated) ERR_CONTRACT_NOT_ACTIVATED) + (asserts! (not (has-mined-at-block stacksHeight userId)) ERR_USER_ALREADY_MINED) + (asserts! (> amountUstx u0) ERR_INSUFFICIENT_COMMITMENT) + (asserts! (>= (stx-get-balance tx-sender) amountUstx) ERR_INSUFFICIENT_BALANCE) + (try! (set-tokens-mined userId stacksHeight amountUstx toStackers toCity)) + (if (is-some memo) + (print memo) + none + ) + (if stackingActive + (try! (stx-transfer? toStackers tx-sender (as-contract tx-sender))) + false + ) + (try! (stx-transfer? toCity tx-sender (var-get cityWallet))) + (ok true) + ) +) + +(define-private (set-tokens-mined (userId uint) (stacksHeight uint) (amountUstx uint) (toStackers uint) (toCity uint)) + (let + ( + (blockStats (get-mining-stats-at-block-or-default stacksHeight)) + (newMinersCount (+ (get minersCount blockStats) u1)) + (minerLowVal (get-last-high-value-at-block stacksHeight)) + (rewardCycle (unwrap! (get-reward-cycle stacksHeight) + ERR_STACKING_NOT_AVAILABLE)) + (rewardCycleStats (get-stacking-stats-at-cycle-or-default rewardCycle)) + ) + (map-set MiningStatsAtBlock + stacksHeight + { + minersCount: newMinersCount, + amount: (+ (get amount blockStats) amountUstx), + amountToCity: (+ (get amountToCity blockStats) toCity), + amountToStackers: (+ (get amountToStackers blockStats) toStackers), + rewardClaimed: false + } + ) + (map-set MinersAtBlock + { + stacksHeight: stacksHeight, + userId: userId + } + { + ustx: amountUstx, + lowValue: (if (> minerLowVal u0) (+ minerLowVal u1) u0), + highValue: (+ minerLowVal amountUstx), + winner: false + } + ) + (map-set MinersAtBlockHighValue + stacksHeight + (+ minerLowVal amountUstx) + ) + (if (> toStackers u0) + (map-set StackingStatsAtCycle + rewardCycle + { + amountUstx: (+ (get amountUstx rewardCycleStats) toStackers), + amountToken: (get amountToken rewardCycleStats) + } + ) + false + ) + (ok true) + ) +) + +;; MINING REWARD CLAIM ACTIONS + +;; calls function to claim mining reward in active logic contract +(define-public (claim-mining-reward (minerBlockHeight uint)) + (begin + (asserts! (or (is-eq (var-get shutdownHeight) u0) (< minerBlockHeight (var-get shutdownHeight))) ERR_CLAIM_IN_WRONG_CONTRACT) + (try! (claim-mining-reward-at-block tx-sender block-height minerBlockHeight)) + (ok true) + ) +) + +;; Determine whether or not the given principal can claim the mined tokens at a particular block height, +;; given the miners record for that block height, a random sample, and the current block height. +(define-private (claim-mining-reward-at-block (user principal) (stacksHeight uint) (minerBlockHeight uint)) + (let + ( + (maturityHeight (+ (var-get tokenRewardMaturity) minerBlockHeight)) + (userId (unwrap! (get-user-id user) ERR_USER_ID_NOT_FOUND)) + (blockStats (unwrap! (get-mining-stats-at-block minerBlockHeight) ERR_NO_MINERS_AT_BLOCK)) + (minerStats (unwrap! (get-miner-at-block minerBlockHeight userId) ERR_USER_DID_NOT_MINE_IN_BLOCK)) + (isMature (asserts! (> stacksHeight maturityHeight) ERR_CLAIMED_BEFORE_MATURITY)) + (vrfSample (unwrap! (contract-call? 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-vrf-v2 get-save-rnd maturityHeight) ERR_NO_VRF_SEED_FOUND)) + (commitTotal (get-last-high-value-at-block minerBlockHeight)) + (winningValue (mod vrfSample commitTotal)) + ) + (asserts! (not (get rewardClaimed blockStats)) ERR_REWARD_ALREADY_CLAIMED) + (asserts! (and (>= winningValue (get lowValue minerStats)) (<= winningValue (get highValue minerStats))) + ERR_MINER_DID_NOT_WIN) + (try! (set-mining-reward-claimed userId minerBlockHeight)) + (ok true) + ) +) + +(define-private (set-mining-reward-claimed (userId uint) (minerBlockHeight uint)) + (let + ( + (blockStats (get-mining-stats-at-block-or-default minerBlockHeight)) + (minerStats (get-miner-at-block-or-default minerBlockHeight userId)) + (user (unwrap! (get-user userId) ERR_USER_NOT_FOUND)) + ) + (map-set MiningStatsAtBlock + minerBlockHeight + { + minersCount: (get minersCount blockStats), + amount: (get amount blockStats), + amountToCity: (get amountToCity blockStats), + amountToStackers: (get amountToStackers blockStats), + rewardClaimed: true + } + ) + (map-set MinersAtBlock + { + stacksHeight: minerBlockHeight, + userId: userId + } + { + ustx: (get ustx minerStats), + lowValue: (get lowValue minerStats), + highValue: (get highValue minerStats), + winner: true + } + ) + (map-set BlockWinnerIds + minerBlockHeight + userId + ) + (try! (mint-coinbase user minerBlockHeight)) + (ok true) + ) +) + +(define-read-only (is-block-winner (user principal) (minerBlockHeight uint)) + (is-block-winner-and-can-claim user minerBlockHeight false) +) + +(define-read-only (can-claim-mining-reward (user principal) (minerBlockHeight uint)) + (is-block-winner-and-can-claim user minerBlockHeight true) +) + +(define-private (is-block-winner-and-can-claim (user principal) (minerBlockHeight uint) (testCanClaim bool)) + (let + ( + (userId (unwrap! (get-user-id user) false)) + (blockStats (unwrap! (get-mining-stats-at-block minerBlockHeight) false)) + (minerStats (unwrap! (get-miner-at-block minerBlockHeight userId) false)) + (maturityHeight (+ (var-get tokenRewardMaturity) minerBlockHeight)) + (vrfSample (unwrap! (contract-call? 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-vrf-v2 get-rnd maturityHeight) false)) + (commitTotal (get-last-high-value-at-block minerBlockHeight)) + (winningValue (mod vrfSample commitTotal)) + ) + (if (and (>= winningValue (get lowValue minerStats)) (<= winningValue (get highValue minerStats))) + (if testCanClaim (not (get rewardClaimed blockStats)) true) + false + ) + ) +) + +;; STACKING CONFIGURATION + +(define-constant MAX_REWARD_CYCLES u32) +(define-constant REWARD_CYCLE_INDEXES (list u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11 u12 u13 u14 u15 u16 u17 u18 u19 u20 u21 u22 u23 u24 u25 u26 u27 u28 u29 u30 u31)) + +;; how long a reward cycle is +(define-data-var rewardCycleLength uint u20) ;; TESTNET: set to 20 blocks + +;; At a given reward cycle: +;; - how many Stackers were there +;; - what is the total uSTX submitted by miners +;; - what is the total amount of tokens stacked +(define-map StackingStatsAtCycle + uint + { + amountUstx: uint, + amountToken: uint + } +) + +;; returns the total stacked tokens and committed uSTX for a given reward cycle +(define-read-only (get-stacking-stats-at-cycle (rewardCycle uint)) + (map-get? StackingStatsAtCycle rewardCycle) +) + +;; returns the total stacked tokens and committed uSTX for a given reward cycle +;; or, an empty structure +(define-read-only (get-stacking-stats-at-cycle-or-default (rewardCycle uint)) + (default-to { amountUstx: u0, amountToken: u0 } + (map-get? StackingStatsAtCycle rewardCycle)) +) + +;; At a given reward cycle and user ID: +;; - what is the total tokens Stacked? +;; - how many tokens should be returned? (based on Stacking period) +(define-map StackerAtCycle + { + rewardCycle: uint, + userId: uint + } + { + amountStacked: uint, + toReturn: uint + } +) + +(define-read-only (get-stacker-at-cycle (rewardCycle uint) (userId uint)) + (map-get? StackerAtCycle { rewardCycle: rewardCycle, userId: userId }) +) + +(define-read-only (get-stacker-at-cycle-or-default (rewardCycle uint) (userId uint)) + (default-to { amountStacked: u0, toReturn: u0 } + (map-get? StackerAtCycle { rewardCycle: rewardCycle, userId: userId })) +) + +;; get the reward cycle for a given Stacks block height +(define-read-only (get-reward-cycle (stacksHeight uint)) + (let + ( + (firstStackingBlock (var-get activationBlock)) + (rcLen (var-get rewardCycleLength)) + ) + (if (>= stacksHeight firstStackingBlock) + (some (/ (- stacksHeight firstStackingBlock) rcLen)) + none) + ) +) + +;; determine if stacking is active in a given cycle +(define-read-only (stacking-active-at-cycle (rewardCycle uint)) + (is-some + (get amountToken (map-get? StackingStatsAtCycle rewardCycle)) + ) +) + +;; get the first Stacks block height for a given reward cycle. +(define-read-only (get-first-stacks-block-in-reward-cycle (rewardCycle uint)) + (+ (var-get activationBlock) (* (var-get rewardCycleLength) rewardCycle)) +) + +;; getter for get-entitled-stacking-reward that specifies block height +(define-read-only (get-stacking-reward (userId uint) (targetCycle uint)) + (get-entitled-stacking-reward userId targetCycle block-height) +) + +;; get uSTX a Stacker can claim, given reward cycle they stacked in and current block height +;; this method only returns a positive value if: +;; - the current block height is in a subsequent reward cycle +;; - the stacker actually locked up tokens in the target reward cycle +;; - the stacker locked up _enough_ tokens to get at least one uSTX +;; it is possible to Stack tokens and not receive uSTX: +;; - if no miners commit during this reward cycle +;; - the amount stacked by user is too few that you'd be entitled to less than 1 uSTX +(define-private (get-entitled-stacking-reward (userId uint) (targetCycle uint) (stacksHeight uint)) + (let + ( + (rewardCycleStats (get-stacking-stats-at-cycle-or-default targetCycle)) + (stackerAtCycle (get-stacker-at-cycle-or-default targetCycle userId)) + (totalUstxThisCycle (get amountUstx rewardCycleStats)) + (totalStackedThisCycle (get amountToken rewardCycleStats)) + (userStackedThisCycle (get amountStacked stackerAtCycle)) + ) + (match (get-reward-cycle stacksHeight) + currentCycle + (if (and (not (var-get isShutdown)) + (or (<= currentCycle targetCycle) (is-eq u0 userStackedThisCycle))) + ;; the contract is not shut down and + ;; this cycle hasn't finished + ;; or stacker contributed nothing + u0 + ;; (totalUstxThisCycle * userStackedThisCycle) / totalStackedThisCycle + (/ (* totalUstxThisCycle userStackedThisCycle) totalStackedThisCycle) + ) + ;; before first reward cycle + u0 + ) + ) +) + +;; STACKING ACTIONS + +(define-public (stack-tokens (amountTokens uint) (lockPeriod uint)) + (let + ( + (userId (get-or-create-user-id tx-sender)) + ) + (try! (stack-tokens-at-cycle tx-sender userId amountTokens block-height lockPeriod)) + (ok true) + ) +) + +(define-private (stack-tokens-at-cycle (user principal) (userId uint) (amountTokens uint) (startHeight uint) (lockPeriod uint)) + (let + ( + (currentCycle (unwrap! (get-reward-cycle startHeight) ERR_STACKING_NOT_AVAILABLE)) + (targetCycle (+ u1 currentCycle)) + (commitment { + stackerId: userId, + amount: amountTokens, + first: targetCycle, + last: (+ targetCycle lockPeriod) + }) + ) + (asserts! (is-activated) ERR_CONTRACT_NOT_ACTIVATED) + (asserts! (and (> lockPeriod u0) (<= lockPeriod MAX_REWARD_CYCLES)) + ERR_CANNOT_STACK) + (asserts! (> amountTokens u0) ERR_CANNOT_STACK) + (try! (contract-call? 'STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-token-v2 transfer amountTokens tx-sender (as-contract tx-sender) none)) + (print { + firstCycle: targetCycle, + lastCycle: (- (+ targetCycle lockPeriod) u1) + }) + (match (fold stack-tokens-closure REWARD_CYCLE_INDEXES (ok commitment)) + okValue (ok true) + errValue (err errValue) + ) + ) +) + +(define-private (stack-tokens-closure (rewardCycleIdx uint) + (commitmentResponse (response + { + stackerId: uint, + amount: uint, + first: uint, + last: uint + } + uint + ))) + + (match commitmentResponse + commitment + (let + ( + (stackerId (get stackerId commitment)) + (amountToken (get amount commitment)) + (firstCycle (get first commitment)) + (lastCycle (get last commitment)) + (targetCycle (+ firstCycle rewardCycleIdx)) + ) + (begin + (if (and (>= targetCycle firstCycle) (< targetCycle lastCycle)) + (begin + (if (is-eq targetCycle (- lastCycle u1)) + (set-tokens-stacked stackerId targetCycle amountToken amountToken) + (set-tokens-stacked stackerId targetCycle amountToken u0) + ) + true + ) + false + ) + commitmentResponse + ) + ) + errValue commitmentResponse + ) +) + +(define-private (set-tokens-stacked (userId uint) (targetCycle uint) (amountStacked uint) (toReturn uint)) + (let + ( + (rewardCycleStats (get-stacking-stats-at-cycle-or-default targetCycle)) + (stackerAtCycle (get-stacker-at-cycle-or-default targetCycle userId)) + ) + (map-set StackingStatsAtCycle + targetCycle + { + amountUstx: (get amountUstx rewardCycleStats), + amountToken: (+ amountStacked (get amountToken rewardCycleStats)) + } + ) + (map-set StackerAtCycle + { + rewardCycle: targetCycle, + userId: userId + } + { + amountStacked: (+ amountStacked (get amountStacked stackerAtCycle)), + toReturn: (+ toReturn (get toReturn stackerAtCycle)) + } + ) + ) +) + +;; STACKING REWARD CLAIMS + +;; calls function to claim stacking reward in active logic contract +(define-public (claim-stacking-reward (targetCycle uint)) + (begin + (try! (claim-stacking-reward-at-cycle tx-sender block-height targetCycle)) + (ok true) + ) +) + +(define-private (claim-stacking-reward-at-cycle (user principal) (stacksHeight uint) (targetCycle uint)) + (let + ( + (currentCycle (unwrap! (get-reward-cycle stacksHeight) ERR_STACKING_NOT_AVAILABLE)) + (userId (unwrap! (get-user-id user) ERR_USER_ID_NOT_FOUND)) + (entitledUstx (get-entitled-stacking-reward userId targetCycle stacksHeight)) + (stackerAtCycle (get-stacker-at-cycle-or-default targetCycle userId)) + (toReturn (get toReturn stackerAtCycle)) + ) + (asserts! (or + (is-eq true (var-get isShutdown)) + (> currentCycle targetCycle)) + ERR_REWARD_CYCLE_NOT_COMPLETED) + (asserts! (or (> toReturn u0) (> entitledUstx u0)) ERR_NOTHING_TO_REDEEM) + ;; disable ability to claim again + (map-set StackerAtCycle + { + rewardCycle: targetCycle, + userId: userId + } + { + amountStacked: u0, + toReturn: u0 + } + ) + ;; send back tokens if user was eligible + (if (> toReturn u0) + (try! (as-contract (contract-call? 'STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-token-v2 transfer toReturn tx-sender user none))) + true + ) + ;; send back rewards if user was eligible + (if (> entitledUstx u0) + (try! (as-contract (stx-transfer? entitledUstx tx-sender user))) + true + ) + (ok true) + ) +) + +;; TOKEN CONFIGURATION + +;; decimals and multiplier for token +(define-constant DECIMALS u6) +(define-constant MICRO_CITYCOINS (pow u10 DECIMALS)) + +;; bonus period length for increased coinbase rewards +(define-constant TOKEN_BONUS_PERIOD u10000) + +;; coinbase thresholds per halving, used to determine halvings +(define-data-var coinbaseThreshold1 uint u0) +(define-data-var coinbaseThreshold2 uint u0) +(define-data-var coinbaseThreshold3 uint u0) +(define-data-var coinbaseThreshold4 uint u0) +(define-data-var coinbaseThreshold5 uint u0) + +;; return coinbase thresholds if contract activated +(define-read-only (get-coinbase-thresholds) + (let + ( + (activated (get-activation-status)) + ) + (asserts! activated ERR_CONTRACT_NOT_ACTIVATED) + (ok { + coinbaseThreshold1: (var-get coinbaseThreshold1), + coinbaseThreshold2: (var-get coinbaseThreshold2), + coinbaseThreshold3: (var-get coinbaseThreshold3), + coinbaseThreshold4: (var-get coinbaseThreshold4), + coinbaseThreshold5: (var-get coinbaseThreshold5) + }) + ) +) + +;; set coinbase thresholds, used during activation +(define-private (set-coinbase-thresholds) + (let + ( + (coinbaseThresholds (try! (contract-call? 'STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-token-v2 get-coinbase-thresholds))) + ) + (var-set coinbaseThreshold1 (get coinbaseThreshold1 coinbaseThresholds)) + (var-set coinbaseThreshold2 (get coinbaseThreshold2 coinbaseThresholds)) + (var-set coinbaseThreshold3 (get coinbaseThreshold3 coinbaseThresholds)) + (var-set coinbaseThreshold4 (get coinbaseThreshold4 coinbaseThresholds)) + (var-set coinbaseThreshold5 (get coinbaseThreshold5 coinbaseThresholds)) + ;; print coinbase thresholds + (print { + coinbaseThreshold1: (var-get coinbaseThreshold1), + coinbaseThreshold2: (var-get coinbaseThreshold2), + coinbaseThreshold3: (var-get coinbaseThreshold3), + coinbaseThreshold4: (var-get coinbaseThreshold4), + coinbaseThreshold5: (var-get coinbaseThreshold5) + }) + (ok true) + ) +) + +;; guarded function for auth to update coinbase thresholds +(define-public (update-coinbase-thresholds) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (try! (set-coinbase-thresholds)) + (ok true) + ) +) + +;; coinbase rewards per threshold, used to determine rewards +(define-data-var coinbaseAmountBonus uint u0) +(define-data-var coinbaseAmount1 uint u0) +(define-data-var coinbaseAmount2 uint u0) +(define-data-var coinbaseAmount3 uint u0) +(define-data-var coinbaseAmount4 uint u0) +(define-data-var coinbaseAmount5 uint u0) +(define-data-var coinbaseAmountDefault uint u0) + +;; return coinbase amounts if contract activated +(define-read-only (get-coinbase-amounts) + (let + ( + (activated (get-activation-status)) + ) + (asserts! activated ERR_CONTRACT_NOT_ACTIVATED) + (ok { + coinbaseAmountBonus: (var-get coinbaseAmountBonus), + coinbaseAmount1: (var-get coinbaseAmount1), + coinbaseAmount2: (var-get coinbaseAmount2), + coinbaseAmount3: (var-get coinbaseAmount3), + coinbaseAmount4: (var-get coinbaseAmount4), + coinbaseAmount5: (var-get coinbaseAmount5), + coinbaseAmountDefault: (var-get coinbaseAmountDefault) + }) + ) +) + +;; set coinbase amounts, used during activation +(define-private (set-coinbase-amounts) + (let + ( + (coinbaseAmounts (unwrap! (contract-call? 'STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-token-v2 get-coinbase-amounts) ERR_COINBASE_AMOUNTS_NOT_FOUND)) + ) + (var-set coinbaseAmountBonus (get coinbaseAmountBonus coinbaseAmounts)) + (var-set coinbaseAmount1 (get coinbaseAmount1 coinbaseAmounts)) + (var-set coinbaseAmount2 (get coinbaseAmount2 coinbaseAmounts)) + (var-set coinbaseAmount3 (get coinbaseAmount3 coinbaseAmounts)) + (var-set coinbaseAmount4 (get coinbaseAmount4 coinbaseAmounts)) + (var-set coinbaseAmount5 (get coinbaseAmount5 coinbaseAmounts)) + (var-set coinbaseAmountDefault (get coinbaseAmountDefault coinbaseAmounts)) + ;; print coinbase amounts + (print { + coinbaseAmountBonus: (var-get coinbaseAmountBonus), + coinbaseAmount1: (var-get coinbaseAmount1), + coinbaseAmount2: (var-get coinbaseAmount2), + coinbaseAmount3: (var-get coinbaseAmount3), + coinbaseAmount4: (var-get coinbaseAmount4), + coinbaseAmount5: (var-get coinbaseAmount5), + coinbaseAmountDefault: (var-get coinbaseAmountDefault) + }) + (ok true) + ) +) + +;; guarded function for auth to update coinbase amounts +(define-public (update-coinbase-amounts) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (try! (set-coinbase-amounts)) + (ok true) + ) +) + +;; function for deciding how many tokens to mint, depending on when they were mined +(define-read-only (get-coinbase-amount (minerBlockHeight uint)) + (begin + ;; if contract is not active, return 0 + (asserts! (>= minerBlockHeight (var-get activationBlock)) u0) + ;; if contract is active, return based on emissions schedule + ;; defined in CCIP-008 https://github.com/citycoins/governance + (asserts! (> minerBlockHeight (var-get coinbaseThreshold1)) + (if (<= (- minerBlockHeight (var-get activationBlock)) TOKEN_BONUS_PERIOD) + ;; bonus reward for initial miners + (var-get coinbaseAmountBonus) + ;; standard reward until 1st halving + (var-get coinbaseAmount1) + ) + ) + ;; computations based on each halving threshold + (asserts! (> minerBlockHeight (var-get coinbaseThreshold2)) (var-get coinbaseAmount2)) + (asserts! (> minerBlockHeight (var-get coinbaseThreshold3)) (var-get coinbaseAmount3)) + (asserts! (> minerBlockHeight (var-get coinbaseThreshold4)) (var-get coinbaseAmount4)) + (asserts! (> minerBlockHeight (var-get coinbaseThreshold5)) (var-get coinbaseAmount5)) + ;; default value after 5th halving + (var-get coinbaseAmountDefault) + ) +) + +;; mint new tokens for claimant who won at given Stacks block height +(define-private (mint-coinbase (recipient principal) (stacksHeight uint)) + (as-contract (contract-call? 'STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-token-v2 mint (get-coinbase-amount stacksHeight) recipient)) +) + +;; UTILITIES + +(define-data-var shutdownHeight uint u0) +(define-data-var isShutdown bool false) + +;; stop mining and stacking operations +;; in preparation for a core upgrade +(define-public (shutdown-contract (stacksHeight uint)) + (begin + ;; make sure block height is in the future + (asserts! (>= stacksHeight block-height) ERR_BLOCK_HEIGHT_IN_PAST) + ;; only allow shutdown request from AUTH + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + ;; set variables to disable mining/stacking in CORE + (var-set activationReached false) + (var-set shutdownHeight stacksHeight) + ;; set variable to allow for all stacking claims + (var-set isShutdown true) + (ok true) + ) +) + +;; checks if caller is Auth contract +(define-private (is-authorized-auth) + (is-eq contract-caller 'STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-auth-v2) +) + +;; checks if contract is fully activated to +;; enable mining and stacking functions +(define-private (is-activated) + (and (get-activation-status) (>= block-height (var-get activationTarget))) +) diff --git a/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-core-v2.json b/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-core-v2.json new file mode 100644 index 00000000..89c8a70a --- /dev/null +++ b/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-core-v2.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-token-v2.clar b/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-token-v2.clar new file mode 100644 index 00000000..30efc5fe --- /dev/null +++ b/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-token-v2.clar @@ -0,0 +1,281 @@ +;; NEWYORKCITYCOIN TOKEN V2 CONTRACT TESTNET +;; CityCoins Protocol Version 2.0.0 + +;; TRAIT DEFINITIONS + +(impl-trait 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-token-v2-trait.citycoin-token-v2) + +;; ERROR CODES + +(define-constant ERR_UNAUTHORIZED (err u2000)) +(define-constant ERR_TOKEN_NOT_ACTIVATED (err u2001)) +(define-constant ERR_TOKEN_ALREADY_ACTIVATED (err u2002)) +(define-constant ERR_V1_BALANCE_NOT_FOUND (err u2003)) +(define-constant ERR_INVALID_COINBASE_THRESHOLD (err u2004)) +(define-constant ERR_INVALID_COINBASE_AMOUNT (err u2005)) + +;; SIP-010 DEFINITION + +(impl-trait 'ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.sip-010-trait-ft-standard.sip-010-trait) + +(define-fungible-token newyorkcitycoin) + +(define-constant DECIMALS u6) +(define-constant MICRO_CITYCOINS (pow u10 DECIMALS)) + +;; SIP-010 FUNCTIONS + +(define-public (transfer (amount uint) (from principal) (to principal) (memo (optional (buff 34)))) + (begin + (asserts! (is-eq from tx-sender) ERR_UNAUTHORIZED) + (if (is-some memo) + (print memo) + none + ) + (ft-transfer? newyorkcitycoin amount from to) + ) +) + +(define-read-only (get-name) + (ok "newyorkcitycoin") +) + +(define-read-only (get-symbol) + (ok "NYC") +) + +(define-read-only (get-decimals) + (ok DECIMALS) +) + +(define-read-only (get-balance (user principal)) + (ok (ft-get-balance newyorkcitycoin user)) +) + +(define-read-only (get-total-supply) + (ok (ft-get-supply newyorkcitycoin)) +) + +(define-read-only (get-token-uri) + (ok (var-get tokenUri)) +) + +;; TOKEN CONFIGURATION + +;; define bonus period and initial epoch length +(define-constant TOKEN_BONUS_PERIOD u10000) +(define-constant TOKEN_EPOCH_LENGTH u25000) + +;; once activated, activation cannot happen again +(define-data-var tokenActivated bool false) + +;; core contract states +(define-constant STATE_DEPLOYED u0) +(define-constant STATE_ACTIVE u1) +(define-constant STATE_INACTIVE u2) + +;; one-time function to activate the token +(define-public (activate-token (coreContract principal) (stacksHeight uint)) + (let + ( + (coreContractMap (try! (contract-call? 'STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-auth-v2 get-core-contract-info coreContract))) + (threshold1 (+ stacksHeight TOKEN_BONUS_PERIOD TOKEN_EPOCH_LENGTH)) ;; 35,000 blocks + (threshold2 (+ stacksHeight TOKEN_BONUS_PERIOD (* u3 TOKEN_EPOCH_LENGTH))) ;; 85,000 blocks + (threshold3 (+ stacksHeight TOKEN_BONUS_PERIOD (* u7 TOKEN_EPOCH_LENGTH))) ;; 185,000 blocks + (threshold4 (+ stacksHeight TOKEN_BONUS_PERIOD (* u15 TOKEN_EPOCH_LENGTH))) ;; 385,000 blocks + (threshold5 (+ stacksHeight TOKEN_BONUS_PERIOD (* u31 TOKEN_EPOCH_LENGTH))) ;; 785,000 blocks + ) + (asserts! (is-eq (get state coreContractMap) STATE_ACTIVE) ERR_UNAUTHORIZED) + (asserts! (not (var-get tokenActivated)) ERR_TOKEN_ALREADY_ACTIVATED) + (var-set tokenActivated true) + (try! (set-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5)) + (ok true) + ) +) + +;; COINBASE THRESHOLDS + +;; coinbase thresholds per halving, used to select coinbase rewards in core +;; initially set by register-user in core contract per CCIP-008 +(define-data-var coinbaseThreshold1 uint u0) +(define-data-var coinbaseThreshold2 uint u0) +(define-data-var coinbaseThreshold3 uint u0) +(define-data-var coinbaseThreshold4 uint u0) +(define-data-var coinbaseThreshold5 uint u0) + +;; return coinbase thresholds if token activated +(define-read-only (get-coinbase-thresholds) + (let + ( + (activated (var-get tokenActivated)) + ) + (asserts! activated ERR_TOKEN_NOT_ACTIVATED) + (ok { + coinbaseThreshold1: (var-get coinbaseThreshold1), + coinbaseThreshold2: (var-get coinbaseThreshold2), + coinbaseThreshold3: (var-get coinbaseThreshold3), + coinbaseThreshold4: (var-get coinbaseThreshold4), + coinbaseThreshold5: (var-get coinbaseThreshold5) + }) + ) +) + +(define-private (set-coinbase-thresholds (threshold1 uint) (threshold2 uint) (threshold3 uint) (threshold4 uint) (threshold5 uint)) + (begin + ;; check that all thresholds increase in value + (asserts! (and (> threshold1 u0) (> threshold2 threshold1) (> threshold3 threshold2) (> threshold4 threshold3) (> threshold5 threshold4)) ERR_INVALID_COINBASE_THRESHOLD) + ;; set coinbase thresholds + (var-set coinbaseThreshold1 threshold1) + (var-set coinbaseThreshold2 threshold2) + (var-set coinbaseThreshold3 threshold3) + (var-set coinbaseThreshold4 threshold4) + (var-set coinbaseThreshold5 threshold5) + ;; print coinbase thresholds + (print { + coinbaseThreshold1: threshold1, + coinbaseThreshold2: threshold2, + coinbaseThreshold3: threshold3, + coinbaseThreshold4: threshold4, + coinbaseThreshold5: threshold5 + }) + (ok true) + ) +) + +;; only accessible by auth +(define-public (update-coinbase-thresholds (threshold1 uint) (threshold2 uint) (threshold3 uint) (threshold4 uint) (threshold5 uint)) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (asserts! (var-get tokenActivated) ERR_TOKEN_NOT_ACTIVATED) + (try! (set-coinbase-thresholds threshold1 threshold2 threshold3 threshold4 threshold5)) + (ok true) + ) +) + +;; COINBASE AMOUNTS (REWARDS) + +;; coinbase rewards per threshold per CCIP-008 +(define-data-var coinbaseAmountBonus uint (* MICRO_CITYCOINS u250000)) +(define-data-var coinbaseAmount1 uint (* MICRO_CITYCOINS u100000)) +(define-data-var coinbaseAmount2 uint (* MICRO_CITYCOINS u50000)) +(define-data-var coinbaseAmount3 uint (* MICRO_CITYCOINS u25000)) +(define-data-var coinbaseAmount4 uint (* MICRO_CITYCOINS u12500)) +(define-data-var coinbaseAmount5 uint (* MICRO_CITYCOINS u6250)) +(define-data-var coinbaseAmountDefault uint (* MICRO_CITYCOINS u3125)) + +;; return coinbase thresholds if token activated +(define-read-only (get-coinbase-amounts) + (ok { + coinbaseAmountBonus: (var-get coinbaseAmountBonus), + coinbaseAmount1: (var-get coinbaseAmount1), + coinbaseAmount2: (var-get coinbaseAmount2), + coinbaseAmount3: (var-get coinbaseAmount3), + coinbaseAmount4: (var-get coinbaseAmount4), + coinbaseAmount5: (var-get coinbaseAmount5), + coinbaseAmountDefault: (var-get coinbaseAmountDefault) + }) +) + +(define-private (set-coinbase-amounts (amountBonus uint) (amount1 uint) (amount2 uint) (amount3 uint) (amount4 uint) (amount5 uint) (amountDefault uint)) + (begin + ;; check that all amounts are greater than zero + (asserts! (and (> amountBonus u0) (> amount1 u0) (> amount2 u0) (> amount3 u0) (> amount4 u0) (> amount5 u0) (> amountDefault u0)) ERR_INVALID_COINBASE_AMOUNT) + ;; set coinbase amounts in token contract + (var-set coinbaseAmountBonus amountBonus) + (var-set coinbaseAmount1 amount1) + (var-set coinbaseAmount2 amount2) + (var-set coinbaseAmount3 amount3) + (var-set coinbaseAmount4 amount4) + (var-set coinbaseAmount5 amount5) + (var-set coinbaseAmountDefault amountDefault) + ;; print coinbase amounts + (print { + coinbaseAmountBonus: amountBonus, + coinbaseAmount1: amount1, + coinbaseAmount2: amount2, + coinbaseAmount3: amount3, + coinbaseAmount4: amount4, + coinbaseAmount5: amount5, + coinbaseAmountDefault: amountDefault + }) + (ok true) + ) +) + +;; only accessible by auth +(define-public (update-coinbase-amounts (amountBonus uint) (amount1 uint) (amount2 uint) (amount3 uint) (amount4 uint) (amount5 uint) (amountDefault uint)) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + ;; (asserts! (var-get tokenActivated) ERR_TOKEN_NOT_ACTIVATED) + (try! (set-coinbase-amounts amountBonus amount1 amount2 amount3 amount4 amount5 amountDefault)) + (ok true) + ) +) + +;; V1 TO V2 CONVERSION + +;; TESTNET: convert-to-v2 removed, no v1 + +;; UTILITIES + +(define-data-var tokenUri (optional (string-utf8 256)) (some u"https://cdn.citycoins.co/metadata/newyorkcitycoin.json")) + +;; set token URI to new value, only accessible by Auth +(define-public (set-token-uri (newUri (optional (string-utf8 256)))) + (begin + (asserts! (is-authorized-auth) ERR_UNAUTHORIZED) + (ok (var-set tokenUri newUri)) + ) +) + +;; mint new tokens, only accessible by a Core contract +(define-public (mint (amount uint) (recipient principal)) + (let + ( + (coreContract (try! (contract-call? 'STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-auth-v2 get-core-contract-info contract-caller))) + ) + (ft-mint? newyorkcitycoin amount recipient) + ) +) + +;; burn tokens +(define-public (burn (amount uint) (owner principal)) + (begin + (asserts! (is-eq tx-sender owner) ERR_UNAUTHORIZED) + (ft-burn? newyorkcitycoin amount owner) + ) +) + +;; checks if caller is Auth contract +(define-private (is-authorized-auth) + (is-eq contract-caller 'STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-auth-v2) +) + +;; SEND-MANY + +(define-public (send-many (recipients (list 200 { to: principal, amount: uint, memo: (optional (buff 34)) }))) + (fold check-err + (map send-token recipients) + (ok true) + ) +) + +(define-private (check-err (result (response bool uint)) (prior (response bool uint))) + (match prior ok-value + result + err-value (err err-value) + ) +) + +(define-private (send-token (recipient { to: principal, amount: uint, memo: (optional (buff 34)) })) + (send-token-with-memo (get amount recipient) (get to recipient) (get memo recipient)) +) + +(define-private (send-token-with-memo (amount uint) (to principal) (memo (optional (buff 34)))) + (let + ( + (transferOk (try! (transfer amount tx-sender to memo))) + ) + (ok transferOk) + ) +) diff --git a/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-token-v2.json b/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-token-v2.json new file mode 100644 index 00000000..89c8a70a --- /dev/null +++ b/.cache/requirements/STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-token-v2.json @@ -0,0 +1,4 @@ +{ + "epoch": "Epoch2_05", + "clarity_version": "Clarity1" +} \ No newline at end of file diff --git a/.github/workflows/test-contracts.yaml b/.github/workflows/clarinet-legacy.yaml similarity index 74% rename from .github/workflows/test-contracts.yaml rename to .github/workflows/clarinet-legacy.yaml index 884badd1..2f2aae63 100644 --- a/.github/workflows/test-contracts.yaml +++ b/.github/workflows/clarinet-legacy.yaml @@ -1,7 +1,4 @@ -# GitHub action to run Clarinet tests against the contracts -# and fail if code is invalid or a test fails. - -name: Test Contracts +name: Test contracts with clarinet (legacy) on: push: @@ -19,16 +16,16 @@ env: CLARINET_DISABLE_HINTS: 1 jobs: - test-contracts: + test-contracts-legacy: runs-on: ubuntu-latest steps: - name: "Checkout code" uses: actions/checkout@v4 - - name: "Check contract syntax" + - name: "Check contract syntax (legacy)" uses: docker://hirosystems/clarinet:1.8.0 with: args: check - - name: "Run all contract tests" + - name: "Run all contract tests (legacy)" uses: docker://hirosystems/clarinet:1.8.0 with: args: test --coverage diff --git a/.github/workflows/clarinet.yaml b/.github/workflows/clarinet.yaml new file mode 100644 index 00000000..e7612526 --- /dev/null +++ b/.github/workflows/clarinet.yaml @@ -0,0 +1,28 @@ +name: Test contracts with clarinet + +on: + push: + branches: + - main + - develop + pull_request: + branches: + - main + - develop + # allow running manually + workflow_dispatch: + +jobs: + test-contracts: + runs-on: ubuntu-latest + steps: + - name: "Checkout code" + uses: actions/checkout@v4 + - name: "Configure clarinet" + run: | + mkdir -p ~/.clarinet + echo "enable_hints = false" >> ~/.clarinet/clarinetrc.toml + - name: "Check contract syntax" + uses: docker://hirosystems/clarinet:2.6.0 + with: + args: check diff --git a/.gitignore b/.gitignore index c1cda78a..94324556 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,16 @@ **/settings/Mainnet.toml **/settings/Testnet.toml -.cache/ + +# removing to resolve issue with testnet reset +# will use files obtained locally beforehand +# .cache/ +# still want to ignore these two +.cache/deps +.cache/gen +# .cache/requirements will upload contracts + .requirements/ /coverage_report history.txt -coverage.lcov \ No newline at end of file +coverage.lcov diff --git a/Clarinet.toml b/Clarinet.toml index 56ef02f8..a16a86ea 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -140,6 +140,11 @@ path = "contracts/extensions/ccd010-core-v2-adapter.clar" [contracts.ccd011-stacking-payouts] path = "contracts/extensions/ccd011-stacking-payouts.clar" +[contracts.ccd012-redemption-nyc] +path = "contracts/extensions/ccd012-redemption-nyc.clar" +clarity_version = 2 +epoch = 2.4 + [contracts.ccip012-bootstrap] path = "contracts/proposals/ccip012-bootstrap.clar" @@ -174,6 +179,11 @@ path = "contracts/proposals/ccip021-extend-sunset-period-2.clar" clarity_version = 2 epoch = 2.4 +[contracts.ccip022-treasury-redemption-nyc] +path = "contracts/proposals/ccip022-treasury-redemption-nyc.clar" +clarity_version = 2 +epoch = 2.4 + # CITYCOINS PROTOCOL TRAITS [contracts.extension-trait] @@ -208,6 +218,9 @@ path = "tests/contracts/external/test-ccext-governance-token-mia.clar" [contracts.test-ccext-governance-token-nyc] path = "tests/contracts/external/test-ccext-governance-token-nyc.clar" +[contracts.test-ccext-governance-token-nyc-v1] +path = "tests/contracts/external/test-ccext-governance-token-nyc-v1.clar" + [contracts.test-ccext-nft-mia] path = "tests/contracts/external/test-ccext-nft-mia.clar" @@ -522,6 +535,31 @@ path = "tests/contracts/proposals/test-ccip020-shutdown-001.clar" clarity_version = 2 epoch = 2.4 +[contracts.test-ccip022-treasury-redemption-nyc-001] +path = "tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-001.clar" +clarity_version = 2 +epoch = 2.4 + +[contracts.test-ccip022-treasury-redemption-nyc-002] +path = "tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-002.clar" +clarity_version = 2 +epoch = 2.4 + +[contracts.test-ccip022-treasury-redemption-nyc-003] +path = "tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-003.clar" +clarity_version = 2 +epoch = 2.4 + +[contracts.test-ccip022-treasury-redemption-nyc-004] +path = "tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-004.clar" +clarity_version = 2 +epoch = 2.4 + +[contracts.test-ccip022-treasury-redemption-nyc-005] +path = "tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-005.clar" +clarity_version = 2 +epoch = 2.4 + [repl] costs_version = 2 parser_version = 2 diff --git a/contracts/extensions/ccd012-redemption-nyc.clar b/contracts/extensions/ccd012-redemption-nyc.clar new file mode 100644 index 00000000..9ad3b8c4 --- /dev/null +++ b/contracts/extensions/ccd012-redemption-nyc.clar @@ -0,0 +1,278 @@ +;; Title: CCD012 - CityCoin Redemption (NYC) +;; Version: 1.0.0 +;; Summary: A redemption extension that allows users to redeem CityCoins for a portion of the city treasury. +;; Description: An extension that provides the ability to claim a portion of the city treasury in exchange for CityCoins. + +;; TRAITS + +(impl-trait .extension-trait.extension-trait) + +;; CONSTANTS + +;; error codes +(define-constant ERR_UNAUTHORIZED (err u12000)) +(define-constant ERR_PANIC (err u12001)) +(define-constant ERR_GETTING_TOTAL_SUPPLY (err u12002)) +(define-constant ERR_GETTING_REDEMPTION_BALANCE (err u12003)) +(define-constant ERR_ALREADY_ENABLED (err u12004)) +(define-constant ERR_NOT_ENABLED (err u12005)) +(define-constant ERR_BALANCE_NOT_FOUND (err u12006)) +(define-constant ERR_NOTHING_TO_REDEEM (err u12007)) +(define-constant ERR_ALREADY_CLAIMED (err u12008)) +(define-constant ERR_SUPPLY_CALCULATION (err u12009)) + +;; helpers +(define-constant SELF (as-contract tx-sender)) +(define-constant MICRO_CITYCOINS (pow u10 u6)) ;; 6 decimal places +(define-constant REDEMPTION_SCALE_FACTOR (pow u10 u8)) ;; 8 decimal places + +;; DATA VARS + +(define-data-var redemptionsEnabled bool false) +(define-data-var blockHeight uint u0) +(define-data-var totalSupply uint u0) +(define-data-var contractBalance uint u0) +(define-data-var redemptionRatio uint u0) +(define-data-var totalRedeemed uint u0) + +;; DATA MAPS + +;; track totals per principal +(define-map RedemptionClaims + principal ;; address + uint ;; total redemption amount +) + +;; PUBLIC FUNCTIONS + +(define-public (is-dao-or-extension) + (ok (asserts! (or (is-eq tx-sender .base-dao) + (contract-call? .base-dao is-extension contract-caller)) ERR_UNAUTHORIZED + )) +) + +(define-public (callback (sender principal) (memo (buff 34))) + (ok true) +) + +;; initialize contract after deployment to start redemptions +(define-public (initialize-redemption) + (let + ( + ;; MAINNET: SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token + ;; MAINNET: SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 + (nycTotalSupplyV1 (unwrap! (contract-call? .test-ccext-governance-token-nyc-v1 get-total-supply) ERR_PANIC)) + (nycTotalSupplyV2 (unwrap! (contract-call? .test-ccext-governance-token-nyc get-total-supply) ERR_PANIC)) + (nycTotalSupply (+ (* nycTotalSupplyV1 MICRO_CITYCOINS) nycTotalSupplyV2)) + (nycRedemptionBalance (get-redemption-contract-current-balance)) + (nycRedemptionRatio (calculate-redemption-ratio nycRedemptionBalance nycTotalSupply)) + ) + ;; check if sender is DAO or extension + (try! (is-dao-or-extension)) + ;; check that total supply is greater than 0 + (asserts! (> nycTotalSupply u0) ERR_GETTING_TOTAL_SUPPLY) + ;; check that redemption balance is greater than 0 + (asserts! (> nycRedemptionBalance u0) ERR_GETTING_REDEMPTION_BALANCE) + ;; check that redemption ratio has a value + (asserts! (is-some nycRedemptionRatio) ERR_SUPPLY_CALCULATION) + ;; check if redemptions are already enabled + (asserts! (not (var-get redemptionsEnabled)) ERR_ALREADY_ENABLED) + ;; record current block height + (var-set blockHeight block-height) + ;; record total supply at block height + (var-set totalSupply nycTotalSupply) + ;; record contract balance at block height + (var-set contractBalance nycRedemptionBalance) + ;; calculate redemption ratio + (var-set redemptionRatio (unwrap-panic nycRedemptionRatio)) + ;; set redemptionsEnabled to true, can only run once + (var-set redemptionsEnabled true) + ;; print redemption info + (ok (print { + notification: "intialize-contract", + payload: (get-redemption-info) + })) + ) +) + +(define-public (redeem-nyc) + (let + ( + (userAddress tx-sender) + ;; MAINNET: SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token + ;; MAINNET: SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 + (balanceV1 (unwrap! (contract-call? .test-ccext-governance-token-nyc-v1 get-balance userAddress) ERR_BALANCE_NOT_FOUND)) + (balanceV2 (unwrap! (contract-call? .test-ccext-governance-token-nyc get-balance userAddress) ERR_BALANCE_NOT_FOUND)) + (totalBalance (+ (* balanceV1 MICRO_CITYCOINS) balanceV2)) + (redemptionAmount (get-redemption-for-balance totalBalance)) + (redemptionClaimed (default-to u0 (get-redemption-amount-claimed userAddress))) + ) + ;; check if redemptions are enabled + (asserts! (var-get redemptionsEnabled) ERR_NOT_ENABLED) + ;; check that user has at least one positive balance + (asserts! (> (+ balanceV1 balanceV2) u0) ERR_BALANCE_NOT_FOUND) ;; cheaper, credit: LNow + ;; check that contract has a positive balance + (asserts! (> (get-redemption-contract-current-balance) u0) ERR_NOTHING_TO_REDEEM) + ;; check that redemption amount is > 0 + (asserts! (and (is-some redemptionAmount) (> (unwrap-panic redemptionAmount) u0)) ERR_NOTHING_TO_REDEEM) + ;; burn NYC + ;; MAINNET: SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token + ;; MAINNET: SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 + (and (> balanceV1 u0) (try! (contract-call? .test-ccext-governance-token-nyc-v1 burn balanceV1 userAddress))) + (and (> balanceV2 u0) (try! (contract-call? .test-ccext-governance-token-nyc burn balanceV2 userAddress))) + ;; transfer STX + (try! (as-contract (stx-transfer? (unwrap-panic redemptionAmount) SELF userAddress))) + ;; update redemption claims + (var-set totalRedeemed (+ (var-get totalRedeemed) (unwrap-panic redemptionAmount))) + (map-set RedemptionClaims userAddress (+ redemptionClaimed (unwrap-panic redemptionAmount))) + ;; print redemption info + (print { + notification: "contract-redemption", + payload: (get-redemption-info) + }) + ;; print user redemption info + (print { + notification: "user-redemption", + payload: (try! (get-user-redemption-info userAddress)) + }) + ;; return redemption amount + (ok redemptionAmount) + ) +) + +;; READ ONLY FUNCTIONS + +(define-read-only (is-redemption-enabled) + (var-get redemptionsEnabled) +) + +(define-read-only (get-redemption-block-height) + (var-get blockHeight) +) + +(define-read-only (get-redemption-total-supply) + (var-get totalSupply) +) + +(define-read-only (get-redemption-contract-balance) + (var-get contractBalance) +) + +(define-read-only (get-redemption-contract-current-balance) + (stx-get-balance SELF) +) + +(define-read-only (get-redemption-ratio) + (var-get redemptionRatio) +) + +(define-read-only (get-total-redeemed) + (var-get totalRedeemed) +) + +;; aggregate all exposed vars above +(define-read-only (get-redemption-info) + { + redemptionsEnabled: (is-redemption-enabled), + blockHeight: (get-redemption-block-height), + totalSupply: (get-redemption-total-supply), + contractBalance: (get-redemption-contract-balance), + currentContractBalance: (get-redemption-contract-current-balance), + redemptionRatio: (get-redemption-ratio), + totalRedeemed: (get-total-redeemed) + } +) + +(define-read-only (get-nyc-balances (address principal)) + (let + ( + ;; MAINNET: SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token + ;; MAINNET: SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 + (balanceV1 (unwrap! (contract-call? .test-ccext-governance-token-nyc-v1 get-balance address) ERR_BALANCE_NOT_FOUND)) + (balanceV2 (unwrap! (contract-call? .test-ccext-governance-token-nyc get-balance address) ERR_BALANCE_NOT_FOUND)) + (totalBalance (+ (* balanceV1 MICRO_CITYCOINS) balanceV2)) + ) + (ok { + address: address, + balanceV1: balanceV1, + balanceV2: balanceV2, + totalBalance: totalBalance + }) + ) +) + +(define-read-only (get-redemption-for-balance (balance uint)) + (let + ( + (redemptionAmountScaled (* (var-get redemptionRatio) balance)) + (redemptionAmount (/ redemptionAmountScaled REDEMPTION_SCALE_FACTOR)) + (contractCurrentBalance (get-redemption-contract-current-balance)) + ) + (if (> redemptionAmount u0) + (if (< redemptionAmount contractCurrentBalance) + ;; if redemption amount is less than contract balance, return redemption amount + (some redemptionAmount) + ;; if redemption amount is greater than contract balance, return contract balance + (some contractCurrentBalance) + ) + ;; if redemption amount is 0, return none + none + ) + ) +) + +(define-read-only (get-redemption-amount-claimed (address principal)) + (map-get? RedemptionClaims address) +) + +;; aggregate all exposed vars above +(define-read-only (get-user-redemption-info (address principal)) + (let + ( + (nycBalances (try! (get-nyc-balances address))) + (redemptionAmount (default-to u0 (get-redemption-for-balance (get totalBalance nycBalances)))) + (redemptionClaims (default-to u0 (get-redemption-amount-claimed address))) + ) + (ok { + address: address, + nycBalances: nycBalances, + redemptionAmount: redemptionAmount, + redemptionClaims: redemptionClaims + }) + ) +) + +;; PRIVATE FUNCTIONS + +;; CREDIT: ALEX math-fixed-point-16.clar + +(define-private (scale-up (a uint)) + (* a REDEMPTION_SCALE_FACTOR) +) + +;; modified to favor the user when scaling down +(define-private (scale-down (a uint)) + (let + ( + (quotient (/ a REDEMPTION_SCALE_FACTOR)) + (remainder (mod a REDEMPTION_SCALE_FACTOR)) + ) + (if (> remainder u0) + (+ quotient u1) + quotient + ) + ) +) + + +(define-private (calculate-redemption-ratio (balance uint) (supply uint)) + (if (or (is-eq supply u0) (is-eq balance u0)) + none + (let + ( + (scaledBalance (* balance REDEMPTION_SCALE_FACTOR)) + ) + (some (/ scaledBalance supply)) + ) + ) +) diff --git a/contracts/proposals/ccip022-treasury-redemption-nyc.clar b/contracts/proposals/ccip022-treasury-redemption-nyc.clar new file mode 100644 index 00000000..405ae889 --- /dev/null +++ b/contracts/proposals/ccip022-treasury-redemption-nyc.clar @@ -0,0 +1,267 @@ +;; TRAITS + +(impl-trait .proposal-trait.proposal-trait) +(impl-trait .ccip015-trait.ccip015-trait) + +;; ERRORS + +(define-constant ERR_PANIC (err u22000)) +(define-constant ERR_SAVING_VOTE (err u22001)) +(define-constant ERR_VOTED_ALREADY (err u22002)) +(define-constant ERR_NOTHING_STACKED (err u22003)) +(define-constant ERR_USER_NOT_FOUND (err u22004)) +(define-constant ERR_PROPOSAL_NOT_ACTIVE (err u22005)) +(define-constant ERR_PROPOSAL_STILL_ACTIVE (err u22006)) +(define-constant ERR_VOTE_FAILED (err u22007)) + +;; CONSTANTS + +(define-constant SELF (as-contract tx-sender)) +(define-constant CCIP_022 { + name: "CityCoins Treasury Redemption (NYC)", + link: "https://github.com/citycoins/governance/blob/feat/add-ccip-022/ccips/ccip-022/ccip-022-citycoins-treasury-redemption-nyc.md", + hash: "2a22deaae6e7c12bb7726c3061a0f20515644ca9", +}) + +(define-constant VOTE_SCALE_FACTOR (pow u10 u16)) ;; 16 decimal places + +;; set city ID +(define-constant NYC_ID (default-to u2 (contract-call? .ccd004-city-registry get-city-id "nyc"))) + +;; 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, + nyc: uint, + } +) +;; PUBLIC FUNCTIONS + +(define-public (execute (sender principal)) + (let ((nycRedemptionBalance (stx-get-balance .ccd002-treasury-nyc-mining-v2))) + ;; check vote is complete/passed + (try! (is-executable)) + ;; update vote variables + (var-set voteEnd block-height) + (var-set voteActive false) + ;; transfer funds to new redemption extension + (try! (contract-call? .ccd002-treasury-nyc-mining-v2 withdraw-stx nycRedemptionBalance .ccd012-redemption-nyc)) + ;; initialize redemption extension + (try! (contract-call? .ccd012-redemption-nyc initialize-redemption)) + (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)) + (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 NYC_ID nycVoteAmount vote true) + (ok true) + ) + ;; if the voterRecord does not exist + (let + ( + (nycVoteAmount (scale-down (default-to u0 (get-nyc-vote voterId true)))) + ) + ;; check that the user has a positive vote + (asserts! (or (> nycVoteAmount u0)) ERR_NOTHING_STACKED) + ;; insert new user vote record + (asserts! (map-insert UserVotes voterId { + vote: vote, + nyc: nycVoteAmount + }) ERR_SAVING_VOTE) + ;; update vote stats for each city + (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)) + (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 the "yes" votes are at least 25% of the total + (asserts! (>= (get totalAmountYes nycRecord) (/ (get totalAmountYes voteTotals) u4)) ERR_VOTE_FAILED) + ;; allow execution + (ok true) + ) +) + +(define-read-only (is-vote-active) + (some (var-get voteActive)) +) + +(define-read-only (get-proposal-info) + (some CCIP_022) +) + +(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-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 + ( + (nycRecord (get-vote-total-nyc-or-default)) + ) + (some { + nyc: nycRecord, + totals: { + totalAmountYes: (get totalAmountYes nycRecord), + totalAmountNo: (get totalAmountNo nycRecord), + totalVotesYes: (get totalVotesYes nycRecord), + totalVotesNo: (get totalVotesNo nycRecord), + } + }) + ) +) + +(define-read-only (get-voter-info (id uint)) + (map-get? UserVotes id) +) + +;; 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 + ( + ;; MAINNET: NYC cycle 82 / first block BTC 838,250 STX 145,643 + ;; cycle 2 / u4500 used in tests + (cycle82Hash (unwrap! (get-block-hash u4500) none)) + (cycle82Data (at-block cycle82Hash (contract-call? .ccd007-citycoin-stacking get-stacker NYC_ID u2 userId))) + (cycle82Amount (get stacked cycle82Data)) + ;; MAINNET: NYC cycle 83 / first block BTC 840,350 STX 147,282 + ;; cycle 3 / u6600 used in tests + (cycle83Hash (unwrap! (get-block-hash u6600) none)) + (cycle83Data (at-block cycle83Hash (contract-call? .ccd007-citycoin-stacking get-stacker NYC_ID u3 userId))) + (cycle83Amount (get stacked cycle83Data)) + ;; NYC vote calculation + (scaledVote (/ (+ (scale-up cycle82Amount) (scale-up cycle83Amount)) u2)) + ) + ;; check that at least one value is positive + (asserts! (or (> cycle82Amount u0) (> cycle83Amount 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 + (if (> voteAmount u0) + ;; handle vote + (if vote + ;; handle yes vote + (map-set CityVotes cityId { + totalAmountYes: (+ voteAmount (get totalAmountYes cityRecord)), + totalVotesYes: (+ u1 (get totalVotesYes cityRecord)), + totalAmountNo: (if changedVote (- (get totalAmountNo cityRecord) voteAmount) (get totalAmountNo cityRecord)), + totalVotesNo: (if changedVote (- (get totalVotesNo cityRecord) u1) (get totalVotesNo cityRecord)) + }) + ;; handle no vote + (map-set CityVotes cityId { + totalAmountYes: (if changedVote (- (get totalAmountYes cityRecord) voteAmount) (get totalAmountYes cityRecord)), + totalVotesYes: (if changedVote (- (get totalVotesYes cityRecord) u1) (get totalVotesYes cityRecord)), + totalAmountNo: (+ voteAmount (get totalAmountNo cityRecord)), + totalVotesNo: (+ u1 (get totalVotesNo cityRecord)), + }) + ) + ;; ignore calls with vote amount equal to 0 + false) + ) +) + +;; 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/extensions/ccd012-redemption-nyc.model.ts b/models/extensions/ccd012-redemption-nyc.model.ts new file mode 100644 index 00000000..c115dd45 --- /dev/null +++ b/models/extensions/ccd012-redemption-nyc.model.ts @@ -0,0 +1,97 @@ +import { Chain, Account, Tx, types, ReadOnlyFn } from "../../utils/deps.ts"; + +export enum ErrCode { + ERR_UNAUTHORIZED = 12000, + ERR_PANIC, + ERR_GETTING_TOTAL_SUPPLY, + ERR_GETTING_REDEMPTION_BALANCE, + ERR_ALREADY_ENABLED, + ERR_NOT_ENABLED, + ERR_BALANCE_NOT_FOUND, + ERR_NOTHING_TO_REDEEM, + ERR_ALREADY_CLAIMED, +} + +export class CCD012RedemptionNyc { + name: string; + static readonly ErrCode = ErrCode; + chain: Chain; + deployer: Account; + + constructor(chain: Chain, deployer: Account) { + this.name = "ccd012-redemption-nyc"; + this.chain = chain; + this.deployer = deployer; + } + + // Authorization + + isDaoOrExtension(): ReadOnlyFn { + return this.callReadOnlyFn("is-dao-or-extension"); + } + + // Internal DAO functions + + initializeRedemption(sender: Account) { + return Tx.contractCall(this.name, "initialize-redemption", [], sender.address); + } + + redeemNyc(sender: Account) { + return Tx.contractCall(this.name, "redeem-nyc", [], sender.address); + } + + // Read only functions + + isRedemptionEnabled(): ReadOnlyFn { + return this.callReadOnlyFn("is-redemption-enabled", []); + } + + getRedemptionBlockHeight(): ReadOnlyFn { + return this.callReadOnlyFn("get-redemption-block-height", []); + } + + getRedemptionTotalSupply(): ReadOnlyFn { + return this.callReadOnlyFn("get-redemption-total-supply", []); + } + + getRedemptionContractBalance(): ReadOnlyFn { + return this.callReadOnlyFn("get-redemption-contract-balance", []); + } + + getRedemptionRatio(): ReadOnlyFn { + return this.callReadOnlyFn("get-redemption-ratio", []); + } + + getRedemptionInfo(): ReadOnlyFn { + return this.callReadOnlyFn("get-redemption-info", []); + } + + getNycBalances(address: string): ReadOnlyFn { + return this.callReadOnlyFn("get-nyc-balances", [types.principal(address)]); + } + + getRedemptionForBalance(balance: number): ReadOnlyFn { + return this.callReadOnlyFn("get-redemption-for-balance", [types.uint(balance)]); + } + + getRedemptionAmountClaimed(address: string): ReadOnlyFn { + return this.callReadOnlyFn("get-redemption-amount-claimed", [types.principal(address)]); + } + + getUserRedemptionInfo(address: string): ReadOnlyFn { + return this.callReadOnlyFn("get-user-redemption-info", [types.principal(address)]); + } + + // Extension callback + + callback(sender: Account, memo: string) { + return Tx.contractCall(this.name, "callback", [types.principal(sender.address), types.buff(memo)], sender.address); + } + + // Utility functions + + private callReadOnlyFn(method: string, args: Array = [], sender: Account = this.deployer): ReadOnlyFn { + const result = this.chain.callReadOnlyFn(this.name, method, args, sender?.address); + return result; + } +} diff --git a/models/proposals/ccip022-treasury-redemption-nyc.model.ts b/models/proposals/ccip022-treasury-redemption-nyc.model.ts new file mode 100644 index 00000000..8ce57027 --- /dev/null +++ b/models/proposals/ccip022-treasury-redemption-nyc.model.ts @@ -0,0 +1,76 @@ +import { Chain, Account, Tx, types, ReadOnlyFn } from "../../utils/deps.ts"; + +enum ErrCode { + ERR_PANIC = 22000, + ERR_SAVING_VOTE, + ERR_VOTED_ALREADY, + ERR_NOTHING_STACKED, + ERR_USER_NOT_FOUND, + ERR_PROPOSAL_NOT_ACTIVE, + ERR_PROPOSAL_STILL_ACTIVE, + ERR_VOTE_FAILED, +} + +export class CCIP022TreasuryRedemptionNYC { + name = "ccip022-treasury-redemption-nyc"; + static readonly ErrCode = ErrCode; + chain: Chain; + deployer: Account; + + constructor(chain: Chain, deployer: Account) { + this.chain = chain; + this.deployer = deployer; + } + + // public functions + + // execute() excluded since called by passProposal and CCD001 + + voteOnProposal(sender: Account, vote: boolean) { + return Tx.contractCall(this.name, "vote-on-proposal", [types.bool(vote)], sender.address); + } + + // read-only functions + + isExecutable() { + return this.callReadOnlyFn("is-executable"); + } + + isVoteActive() { + return this.callReadOnlyFn("is-vote-active"); + } + + getProposalInfo() { + return this.callReadOnlyFn("get-proposal-info"); + } + + getVotePeriod() { + return this.callReadOnlyFn("get-vote-period"); + } + + getVoteTotalNyc() { + return this.callReadOnlyFn("get-vote-total-nyc"); + } + + getVoteTotalNycOrDefault() { + return this.callReadOnlyFn("get-vote-total-nyc-or-default"); + } + + getVoteTotals() { + return this.callReadOnlyFn("get-vote-totals"); + } + + getVoterInfo(userId: number) { + return this.callReadOnlyFn("get-voter-info", [types.uint(userId)]); + } + + getNycVote(userId: number, scaled: boolean) { + return this.callReadOnlyFn("get-nyc-vote", [types.uint(userId), types.bool(scaled)]); + } + + // read-only function helper + private callReadOnlyFn(method: string, args: Array = [], sender: Account = this.deployer): ReadOnlyFn { + const result = this.chain.callReadOnlyFn(this.name, method, args, sender?.address); + return result; + } +} diff --git a/tests/base-dao.test.ts b/tests/base-dao.test.ts index d87fe120..4409e59e 100644 --- a/tests/base-dao.test.ts +++ b/tests/base-dao.test.ts @@ -206,6 +206,7 @@ Clarinet.test({ if (ext === EXTENSIONS.CCD006_CITYCOIN_MINING_V2) continue; // skip, not enabled until CCIP-014 if (ext === EXTENSIONS.CCD002_TREASURY_MIA_MINING_V2) continue; // skip, not enabled until CCIP-014 if (ext === EXTENSIONS.CCD002_TREASURY_NYC_MINING_V2) continue; // skip, not enabled until CCIP-014 + if (ext === EXTENSIONS.CCD012_REDEMPTION_NYC) continue; // skip, not enabled until CCIP-022 //console.log("ext:", ext); //console.log("enabled:", baseDao.isExtension(ext).result); baseDao.isExtension(ext).result.expectBool(true); diff --git a/tests/contracts/external/test-ccext-governance-token-mia.clar b/tests/contracts/external/test-ccext-governance-token-mia.clar index dfb5988d..5e5b691d 100644 --- a/tests/contracts/external/test-ccext-governance-token-mia.clar +++ b/tests/contracts/external/test-ccext-governance-token-mia.clar @@ -38,10 +38,19 @@ )) ) +;; unguarded: simple mint function (define-public (mint (amount uint) (recipient principal)) (ft-mint? miamicoin amount recipient) ) +;; guarded: burn function (by user only) +(define-public (burn (amount uint) (owner principal)) + (begin + (asserts! (or (is-eq tx-sender owner) (is-eq contract-caller owner)) ERR_NOT_TOKEN_OWNER) + (ft-burn? miamicoin amount owner) + ) +) + ;; guarded: mint governance token (define-public (edg-mint (amount uint) (recipient principal)) (begin diff --git a/tests/contracts/external/test-ccext-governance-token-nyc-v1.clar b/tests/contracts/external/test-ccext-governance-token-nyc-v1.clar new file mode 100644 index 00000000..2e566065 --- /dev/null +++ b/tests/contracts/external/test-ccext-governance-token-nyc-v1.clar @@ -0,0 +1,150 @@ +;; Title: Governance Token +;; Version: 0.0.0 +;; Synopsis: Simple SIP-010 asset for clarinet testing. +;; Description: +;; A dummy SIP-010 asset for use in tests. + +;; TRAITS + +(impl-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait) + +;; TOKEN DEFINITIONS + +(define-fungible-token newyorkcitycoin) +(define-fungible-token newyorkcitycoin-locked) + +;; CONSTANTS + +;; error codes +(define-constant ERR_UNAUTHORIZED (err u3000)) +(define-constant ERR_NOT_TOKEN_OWNER (err u4)) + +;; DATA VARS + +(define-data-var token-name (string-ascii 32) "CityCoins Governance Token V1") +(define-data-var token-symbol (string-ascii 10) "NYC") +(define-data-var token-uri (optional (string-utf8 256)) none) +(define-data-var token-decimals uint u0) + +;; PUBLIC FUNCTIONS + +;; authorization check +(define-public (is-dao-or-extension) + (ok (asserts! + (or + (is-eq tx-sender .base-dao) + (contract-call? .base-dao is-extension contract-caller)) + ERR_UNAUTHORIZED + )) +) + +;; unguarded: simple mint function +(define-public (mint (amount uint) (recipient principal)) + (ft-mint? newyorkcitycoin amount recipient) +) + +;; guarded: burn function (by user only) +(define-public (burn (amount uint) (owner principal)) + (begin + (asserts! (or (is-eq tx-sender owner) (is-eq contract-caller owner)) ERR_NOT_TOKEN_OWNER) + (ft-burn? newyorkcitycoin amount owner) + ) +) + +;; guarded: mint governance token +(define-public (edg-mint (amount uint) (recipient principal)) + (begin + (try! (is-dao-or-extension)) + (ft-mint? newyorkcitycoin amount recipient) + ) +) + +;; guarded: mint governance token to multiple recipients +(define-public (edg-mint-many (recipients (list 200 {amount: uint, recipient: principal}))) + (begin + (try! (is-dao-or-extension)) + (ok (map edg-mint-many-iter recipients)) + ) +) + +;; guarded: burn governance token +(define-public (edg-burn (amount uint) (owner principal)) + (begin + (try! (is-dao-or-extension)) + (ft-burn? newyorkcitycoin amount owner) + ) +) + +;; guarded: set token name +(define-public (set-name (new-name (string-ascii 32))) + (begin + (try! (is-dao-or-extension)) + (ok (var-set token-name new-name)) + ) +) + +;; guarded: set token symbol +(define-public (set-symbol (new-symbol (string-ascii 10))) + (begin + (try! (is-dao-or-extension)) + (ok (var-set token-symbol new-symbol)) + ) +) + +;; guarded: set token decimals +(define-public (set-decimals (new-decimals uint)) + (begin + (try! (is-dao-or-extension)) + (ok (var-set token-decimals new-decimals)) + ) +) + +;; guarded: set token uri +(define-public (set-token-uri (new-uri (optional (string-utf8 256)))) + (begin + (try! (is-dao-or-extension)) + (ok (var-set token-uri new-uri)) + ) +) + +;; SIP-010: transfer +(define-public (transfer (amount uint) (sender principal) (recipient principal) (memo (optional (buff 34)))) + (begin + (asserts! (or (is-eq tx-sender sender) (is-eq contract-caller sender)) ERR_NOT_TOKEN_OWNER) + (ft-transfer? newyorkcitycoin amount sender recipient) + ) +) + +;; READ ONLY FUNCTIONS + +;; SIP-010 functions + +(define-read-only (get-name) + (ok (var-get token-name)) +) + +(define-read-only (get-symbol) + (ok (var-get token-symbol)) +) + +(define-read-only (get-decimals) + (ok (var-get token-decimals)) +) + +(define-read-only (get-balance (who principal)) + (ok (+ (ft-get-balance newyorkcitycoin who) (ft-get-balance newyorkcitycoin-locked who))) +) + +(define-read-only (get-total-supply) + (ok (+ (ft-get-supply newyorkcitycoin) (ft-get-supply newyorkcitycoin-locked))) +) + +(define-read-only (get-token-uri) + (ok (var-get token-uri)) +) + +;; PRIVATE FUNCTIONS + +(define-private (edg-mint-many-iter (item {amount: uint, recipient: principal})) + (ft-mint? newyorkcitycoin (get amount item) (get recipient item)) +) diff --git a/tests/contracts/external/test-ccext-governance-token-nyc.clar b/tests/contracts/external/test-ccext-governance-token-nyc.clar index b11757c1..12085c32 100644 --- a/tests/contracts/external/test-ccext-governance-token-nyc.clar +++ b/tests/contracts/external/test-ccext-governance-token-nyc.clar @@ -38,10 +38,19 @@ )) ) +;; unguarded: simple mint function (define-public (mint (amount uint) (recipient principal)) (ft-mint? newyorkcitycoin amount recipient) ) +;; guarded: burn function (by user only) +(define-public (burn (amount uint) (owner principal)) + (begin + (asserts! (or (is-eq tx-sender owner) (is-eq contract-caller owner)) ERR_NOT_TOKEN_OWNER) + (ft-burn? newyorkcitycoin amount owner) + ) +) + ;; guarded: mint governance token (define-public (edg-mint (amount uint) (recipient principal)) (begin diff --git a/tests/contracts/proposals/test-ccd006-citycoin-mining-v2-002.clar b/tests/contracts/proposals/test-ccd006-citycoin-mining-v2-002.clar index 7cd8afc2..e1e0ec69 100644 --- a/tests/contracts/proposals/test-ccd006-citycoin-mining-v2-002.clar +++ b/tests/contracts/proposals/test-ccd006-citycoin-mining-v2-002.clar @@ -8,6 +8,7 @@ (define-public (execute (sender principal)) (begin (try! (contract-call? .ccd005-city-data add-treasury u1 .ccd002-treasury-mia-mining-v2 "mining-v2")) + (try! (contract-call? .ccd005-city-data add-treasury u2 .ccd002-treasury-nyc-mining-v2 "mining-v2")) (ok true) ) ) diff --git a/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-001.clar b/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-001.clar new file mode 100644 index 00000000..7774abf6 --- /dev/null +++ b/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-001.clar @@ -0,0 +1,57 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer +;; Description: +;; Sets up everything required for CCIP-022 + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (begin + ;; enable CityCoins V2 mining extension + (try! (contract-call? .base-dao set-extensions + (list + {extension: .ccd006-citycoin-mining-v2, enabled: true} + {extension: .ccd012-redemption-nyc, enabled: true} + ) + )) + ;; disable v1 mining, enable v2 + (try! (contract-call? .ccd006-citycoin-mining set-mining-enabled false)) + (try! (contract-call? .ccd006-citycoin-mining-v2 set-mining-enabled true)) + ;; test-ccd004-city-registry-001 + (try! (contract-call? .ccd004-city-registry get-or-create-city-id "mia")) + (try! (contract-call? .ccd004-city-registry get-or-create-city-id "nyc")) + ;; test-ccd005-city-data-001 + (try! (contract-call? .ccd005-city-data set-activation-details u1 u1 u1 u5 u1)) + (try! (contract-call? .ccd005-city-data set-activation-details u2 u2 u2 u2 u2)) + ;; test-ccd005-city-data-002 + (try! (contract-call? .ccd005-city-data set-activation-status u1 true)) + (try! (contract-call? .ccd005-city-data set-activation-status u2 true)) + ;; test-ccd006-city-mining-002 + nyc + (try! (contract-call? .ccd005-city-data add-treasury u1 .ccd002-treasury-mia-mining "mining")) + (try! (contract-call? .ccd005-city-data add-treasury u2 .ccd002-treasury-nyc-mining "mining")) + ;; test-ccd006-citycoin-mining-v2-002 + nyc + (try! (contract-call? .ccd005-city-data add-treasury u1 .ccd002-treasury-mia-mining-v2 "mining-v2")) + (try! (contract-call? .ccd005-city-data add-treasury u2 .ccd002-treasury-nyc-mining-v2 "mining-v2")) + ;; test-ccd007-city-stacking-007 + nyc + (try! (contract-call? .ccd005-city-data add-treasury u1 .ccd002-treasury-mia-stacking "stacking")) + (try! (contract-call? .ccd005-city-data add-treasury u2 .ccd002-treasury-nyc-stacking "stacking")) + ;; test-ccd007-city-stacking-010 + nyc + (try! (contract-call? .ccd002-treasury-nyc-stacking set-allowed .test-ccext-governance-token-nyc-v1 true)) + (try! (contract-call? .ccd002-treasury-nyc-stacking set-allowed .test-ccext-governance-token-nyc true)) + ;; test-ccd005-city-data-009 + (try! (contract-call? .ccd005-city-data set-coinbase-amounts u1 u10 u100 u1000 u10000 u100000 u1000000 u10000000)) + ;; test-ccd005-city-data-010 + (try! (contract-call? .ccd005-city-data set-coinbase-thresholds u1 u50 u60 u70 u80 u90)) + ;; test-ccd005-city-data-018 + (try! (contract-call? .ccd005-city-data set-coinbase-details u1 u20 u1)) + ;; same operations for NYC + (try! (contract-call? .ccd005-city-data set-coinbase-amounts u2 u10 u100 u1000 u10000 u100000 u1000000 u10000000)) + (try! (contract-call? .ccd005-city-data set-coinbase-thresholds u2 u50 u60 u70 u80 u90)) + (try! (contract-call? .ccd005-city-data set-coinbase-details u2 u20 u1)) + (ok true) + ) +) + +;; transfer 15M STX to mining treasury contract from deployer +(stx-transfer? u15000000000000 tx-sender .ccd002-treasury-nyc-mining-v2) diff --git a/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-002.clar b/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-002.clar new file mode 100644 index 00000000..054afd5b --- /dev/null +++ b/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-002.clar @@ -0,0 +1,18 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer +;; Description: +;; Prepares everything for CCD012 execution + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (begin + ;; to simulate newyorkcitycoin-token + (try! (contract-call? .test-ccext-governance-token-nyc-v1 mint u10000 'ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5)) ;; 10k NYC + (try! (contract-call? .test-ccext-governance-token-nyc-v1 mint u1000000 'ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG)) ;; 1M NYC + (try! (contract-call? .test-ccext-governance-token-nyc-v1 mint u5000000 'ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC)) ;; 5M NYC + (try! (contract-call? .test-ccext-governance-token-nyc-v1 mint u10000000 'ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND)) ;; 10M NYC + (ok true) + ) +) diff --git a/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-003.clar b/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-003.clar new file mode 100644 index 00000000..f572e9f9 --- /dev/null +++ b/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-003.clar @@ -0,0 +1,21 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer +;; Description: +;; Prepares everything for CCD012 execution + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (begin + ;; to simulate newyorkcitycoin-token-v2 + (try! (contract-call? .test-ccext-governance-token-nyc mint u10000000000 'ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5)) ;; 10K NYC + (try! (contract-call? .test-ccext-governance-token-nyc mint u1000000000000 'ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG)) ;; 1M NYC + (try! (contract-call? .test-ccext-governance-token-nyc mint u5000000000000 'ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC)) ;; 5M NYC + (try! (contract-call? .test-ccext-governance-token-nyc mint u10000000000000 'ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND)) ;; 10M NYC + (try! (contract-call? .test-ccext-governance-token-nyc mint u100000000000000 'ST2REHHS5J3CERCRBEPMGH7921Q6PYKAADT7JP2VB)) ;; 100M NYC + (try! (contract-call? .test-ccext-governance-token-nyc mint u1000000000000000 'ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0)) ;; 1B NYC + (try! (contract-call? .test-ccext-governance-token-nyc mint u4000000000000000 'ST3PF13W7Z0RRM42A8VZRVFQ75SV1K26RXEP8YGKJ)) ;; 4B NYC + (ok true) + ) +) diff --git a/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-004.clar b/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-004.clar new file mode 100644 index 00000000..a8b788ba --- /dev/null +++ b/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-004.clar @@ -0,0 +1,14 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer +;; Description: +;; Tests calling initialize again + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (begin + (try! (contract-call? .ccd012-redemption-nyc initialize-redemption)) + (ok true) + ) +) diff --git a/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-005.clar b/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-005.clar new file mode 100644 index 00000000..05da6f84 --- /dev/null +++ b/tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-005.clar @@ -0,0 +1,16 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer +;; Description: +;; Mints 10K NYC V2 to deployer address + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (begin + ;; to simulate newyorkcitycoin-token-v2 + (try! (contract-call? .test-ccext-governance-token-nyc mint u10000000000 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM)) ;; 10K NYC + (ok true) + ) +) + diff --git a/tests/extensions/ccd006-citycoin-mining.test.ts b/tests/extensions/ccd006-citycoin-mining.test.ts index ed0b1fba..9dd47189 100644 --- a/tests/extensions/ccd006-citycoin-mining.test.ts +++ b/tests/extensions/ccd006-citycoin-mining.test.ts @@ -260,12 +260,14 @@ Clarinet.test({ fn(chain: Chain, accounts: Map) { // arrange const sender = accounts.get("deployer")!; + const wallet_2 = accounts.get("wallet_2")!; const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining"); const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); // balance is 99999999999992 after ccip-014 deployment // new balance is 99997999999992 after ccip-020 test deployment - const expectedBalance = 99997999999992; - const entries = [49998999999996, 49998999999996]; + // switched to another default account not deployer, normal balance + const expectedBalance = 100000000000000; + const entries = Array.from({ length: 2 }, () => expectedBalance / 2); // act @@ -273,7 +275,7 @@ Clarinet.test({ passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); - const block = chain.mineBlock([ccd006CityMining.mine(sender, miaCityName, entries)]); + const block = chain.mineBlock([ccd006CityMining.mine(wallet_2, miaCityName, entries)]); // assert ccd002Treasury.getBalanceStx().result.expectUint(expectedBalance); diff --git a/tests/extensions/ccd012-redemption-nyc.test.ts b/tests/extensions/ccd012-redemption-nyc.test.ts new file mode 100644 index 00000000..eeb3ad17 --- /dev/null +++ b/tests/extensions/ccd012-redemption-nyc.test.ts @@ -0,0 +1,888 @@ +import { CCD007CityStacking } from "../../models/extensions/ccd007-citycoin-stacking.model.ts"; +import { CCD012RedemptionNyc } from "../../models/extensions/ccd012-redemption-nyc.model.ts"; +import { CCIP022TreasuryRedemptionNYC } from "../../models/proposals/ccip022-treasury-redemption-nyc.model.ts"; +import { EXTENSIONS, PROPOSALS, constructAndPassProposal, nyc, parseClarityTuple, passProposal } from "../../utils/common.ts"; +import { Account, Clarinet, Chain, assertEquals, assertAlmostEquals } from "../../utils/deps.ts"; + +// used for asset identifier in detecting burn events +const NYC_V1_TOKEN = "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.test-ccext-governance-token-nyc-v1::newyorkcitycoin"; +const NYC_V2_TOKEN = "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.test-ccext-governance-token-nyc::newyorkcitycoin"; + +// ============================= +// 0. AUTHORIZATION CHECKS +// ============================= + +Clarinet.test({ + name: "ccd012-redemption-nyc: is-dao-or-extension() fails when called directly", + fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const ccd012RedemptionNyc = new CCD012RedemptionNyc(chain, sender); + + // act + + // assert + ccd012RedemptionNyc.isDaoOrExtension().result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_UNAUTHORIZED); + }, +}); + +Clarinet.test({ + name: "ccd012-redemption-nyc: callback() succeeds when called directly", + fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const ccd012RedemptionNyc = new CCD012RedemptionNyc(chain, sender); + + // act + const { receipts } = chain.mineBlock([ccd012RedemptionNyc.callback(sender, "test")]); + + // assert + assertEquals(receipts.length, 1); + receipts[0].result.expectOk().expectBool(true); + }, +}); + +// ============================= +// initialize-redemption() +// ============================= + +Clarinet.test({ + name: "ccd012-redemption-nyc: initialize-redemption() fails with ERR_UNAUTHORIZED when called directly", + fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const ccd012RedemptionNyc = new CCD012RedemptionNyc(chain, sender); + + // act + const initializeBlock = chain.mineBlock([ccd012RedemptionNyc.initializeRedemption(sender)]); + + // assert + assertEquals(initializeBlock.receipts.length, 1); + initializeBlock.receipts[0].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_UNAUTHORIZED); + }, +}); + +// initialize-redemption() fails with ERR_GETTING_TOTAL_SUPPLY if both supplies are 0 +// note: supply is required for voting in CCIP-022, so unreachable + +// initialize-redemption() fails with ERR_GETTING_REDEMPTION_BALANCE if the redemption balance is 0 +// note: transfer fails in CCIP-022 for this case, so unreachable + +Clarinet.test({ + name: "ccd012-redemption-nyc: initialize-redemption() fails with ERR_ALREADY_ENABLED if called more than once", + async fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const user2 = accounts.get("wallet_2")!; + const user3 = accounts.get("wallet_3")!; + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(chain, sender); + + // set stacking parameters + const amountStacked = 500; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + + // initialize contracts + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + + // mint and move funds + passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_003); + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked, lockPeriod)]); + // for length of mineBlock array, expectOk and expectBool(true) + for (let i = 0; i < stackingBlock.receipts.length; i++) { + stackingBlock.receipts[i].result.expectOk().expectBool(true); + } + + // progress the chain to cycle 5 + // votes are counted in cycles 2-3 + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); + + // execute two yes votes, one no vote + const votingBlock = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user2, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user3, false)]); + for (let i = 0; i < votingBlock.receipts.length; i++) { + votingBlock.receipts[i].result.expectOk().expectBool(true); + } + + const executeBlock = passProposal(chain, accounts, PROPOSALS.CCIP_022); + + assertEquals(executeBlock.receipts.length, 3); + for (let i = 0; i < executeBlock.receipts.length; i++) { + executeBlock.receipts[i].result.expectOk().expectUint(i + 1); + } + + const expectedEvent = `{notification: "intialize-contract", payload: {blockHeight: u12611, contractBalance: u15000000000000, currentContractBalance: u15000000000000, redemptionRatio: u292282, redemptionsEnabled: true, totalRedeemed: u0, totalSupply: u5132020000000000}}`; + // redemption ratio obtained through console logging below + // console.log(expectedEvent); + // console.log(executeBlock.receipts[2].events[3].contract_event.value); + + executeBlock.receipts[2].events.expectPrintEvent(EXTENSIONS.CCD012_REDEMPTION_NYC, expectedEvent); + + // act + const secondInit = passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_004); + + // assert + assertEquals(secondInit.receipts.length, 3); + secondInit.receipts[0].result.expectOk().expectUint(1); + secondInit.receipts[1].result.expectOk().expectUint(2); + secondInit.receipts[2].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_ALREADY_ENABLED); + }, +}); + +Clarinet.test({ + name: "ccd012-redemption-nyc: initialize-redemption() succeeds and prints the redemption info", + async fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const user2 = accounts.get("wallet_2")!; + const user3 = accounts.get("wallet_3")!; + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(chain, sender); + + // set stacking parameters + const amountStacked = 500; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + + // initialize contracts + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + + // mint and move funds + passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_003); + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked, lockPeriod)]); + // for length of mineBlock array, expectOk and expectBool(true) + for (let i = 0; i < stackingBlock.receipts.length; i++) { + stackingBlock.receipts[i].result.expectOk().expectBool(true); + } + + // progress the chain to cycle 5 + // votes are counted in cycles 2-3 + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); + + // execute two yes votes, one no vote + const votingBlock = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user2, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user3, false)]); + for (let i = 0; i < votingBlock.receipts.length; i++) { + votingBlock.receipts[i].result.expectOk().expectBool(true); + } + + // act + const executeBlock = passProposal(chain, accounts, PROPOSALS.CCIP_022); + + // assert + assertEquals(executeBlock.receipts.length, 3); + for (let i = 0; i < executeBlock.receipts.length; i++) { + executeBlock.receipts[i].result.expectOk().expectUint(i + 1); + } + + const expectedEvent = `{notification: "intialize-contract", payload: {blockHeight: u12611, contractBalance: u15000000000000, currentContractBalance: u15000000000000, redemptionRatio: u292282, redemptionsEnabled: true, totalRedeemed: u0, totalSupply: u5132020000000000}}`; + // redemption ratio obtained through console logging below + // console.log(expectedEvent); + // console.log(executeBlock.receipts[2].events[3].contract_event.value); + + executeBlock.receipts[2].events.expectPrintEvent(EXTENSIONS.CCD012_REDEMPTION_NYC, expectedEvent); + }, +}); + +// ============================= +// redeem-nyc() +// ============================= + +Clarinet.test({ + name: "ccd012-redemption-nyc: redeem-nyc() fails with ERR_NOTHING_TO_REDEEM if the redemption amount is none", + async fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const ccd012RedemptionNyc = new CCD012RedemptionNyc(chain, sender); + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + + // initialize contracts + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + + // act + const redeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender)]); + + // assert + assertEquals(redeemBlock.receipts.length, 1); + // note: actually fails with ERR_NOTHING_TO_REDEEM, but did not want to remove test case / extra code to be safe + // check was moved from let statement into function body with assert + // before being initialized the redemption amount always returns none, so short-circuits early before error + redeemBlock.receipts[0].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_NOT_ENABLED); + }, +}); + +Clarinet.test({ + name: "ccd012-redemption-nyc: redeem-nyc() fails with ERR_NOT_ENABLED if the redemption is not initialized", + async fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const ccd012RedemptionNyc = new CCD012RedemptionNyc(chain, sender); + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + + // initialize contracts + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_002); + + // act + const redeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender)]); + + // assert + assertEquals(redeemBlock.receipts.length, 1); + redeemBlock.receipts[0].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_NOT_ENABLED); + }, +}); + +// redeem-nyc() fails with ERR_ALREADY_CLAIMED if the redemption is already claimed +// redeem-nyc() fails with ERR_BALANCE_NOT_FOUND if v1 or v2 tokens are not found +// redeem-nyc() fails with ERR_NOTHING_TO_REDEEM if the redemption amount is 0 + +Clarinet.test({ + name: "ccd012-redemption-nyc: redeem-nyc() succeeds with only v1 tokens", + async fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const user2 = accounts.get("wallet_2")!; + const user3 = accounts.get("wallet_3")!; + const user4 = accounts.get("wallet_4")!; + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccd012RedemptionNyc = new CCD012RedemptionNyc(chain, sender); + const ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(chain, sender); + + const amountStacked = 10000000000; // match balance for user + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + + // initialize contracts + const constructBlock = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + for (let i = 0; i < constructBlock.receipts.length; i++) { + if (i === 0) { + constructBlock.receipts[i].result.expectOk().expectBool(true); + } else { + constructBlock.receipts[i].result.expectOk().expectUint(i); + } + } + + const fundV1Block = passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_002); + const fundV2SenderBlock = passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_005); + for (let i = 0; i < fundV1Block.receipts.length; i++) { + fundV1Block.receipts[i].result.expectOk().expectUint(i + 1); + fundV2SenderBlock.receipts[i].result.expectOk().expectUint(i + 1); + } + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(sender, nyc.cityName, amountStacked, lockPeriod)]); + // console.log("stackingBlock", stackingBlock); + for (let i = 0; i < stackingBlock.receipts.length; i++) { + stackingBlock.receipts[i].result.expectOk().expectBool(true); + } + + // 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 one yes vote + const votingBlock = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(sender, true)]); + // console.log("votingBlock", votingBlock); + for (let i = 0; i < votingBlock.receipts.length; i++) { + votingBlock.receipts[i].result.expectOk().expectBool(true); + } + + // execute ccip-022 + const executeBlock = passProposal(chain, accounts, PROPOSALS.CCIP_022); + // console.log("executeBlock", executeBlock); + assertEquals(executeBlock.receipts.length, 3); + for (let i = 0; i < executeBlock.receipts.length; i++) { + executeBlock.receipts[i].result.expectOk().expectUint(i + 1); + } + + // get contract redemption info + + const redemptionInfo = await ccd012RedemptionNyc.getRedemptionInfo().result; + // console.log("v1 only redemptionInfo", parseClarityTuple(redemptionInfo)); + + // get user balances + const user1Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user1.address).result; + const user2Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user2.address).result; + const user3Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user3.address).result; + const user4Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user4.address).result; + + const user1InfoObject = parseClarityTuple(user1Info); + const user2InfoObject = parseClarityTuple(user2Info); + const user3InfoObject = parseClarityTuple(user3Info); + const user4InfoObject = parseClarityTuple(user4Info); + + const userInfoObjects = [user1InfoObject, user2InfoObject, user3InfoObject, user4InfoObject]; + + // act + const redeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender), ccd012RedemptionNyc.redeemNyc(user1), ccd012RedemptionNyc.redeemNyc(user2), ccd012RedemptionNyc.redeemNyc(user3), ccd012RedemptionNyc.redeemNyc(user4)]); + // console.log("v1 only redeem block", redeemBlock); + + // assert + assertEquals(redeemBlock.receipts.length, 5); + for (let i = 0; i < redeemBlock.receipts.length; i++) { + if (i === 0) { + redeemBlock.receipts[i].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_BALANCE_NOT_FOUND); + } else { + redeemBlock.receipts[i].result + .expectOk() + .expectSome() + .expectUint(userInfoObjects[i - 1].redemptionAmount); + const expectedBurnEvent = { + asset_identifier: NYC_V1_TOKEN, + sender: userInfoObjects[i - 1].address, + amount: userInfoObjects[i - 1].nycBalances.balanceV1, + }; + redeemBlock.receipts[i].events.expectFungibleTokenBurnEvent(expectedBurnEvent.amount, expectedBurnEvent.sender, expectedBurnEvent.asset_identifier); + } + } + // console.log("----------"); + // console.log("user1Info", user1InfoObject); + // console.log("user2Info", user2InfoObject); + // console.log("user3Info", user3InfoObject); + // console.log("user4Info", user4InfoObject); + // console.log("----------"); + }, +}); + +Clarinet.test({ + name: "ccd012-redemption-nyc: redeem-nyc() succeeds with only v2 tokens", + async fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const user2 = accounts.get("wallet_2")!; + const user3 = accounts.get("wallet_3")!; + const user4 = accounts.get("wallet_4")!; + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccd012RedemptionNyc = new CCD012RedemptionNyc(chain, sender); + const ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(chain, sender); + + const amountStacked = 500; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + + // initialize contracts + const constructBlock = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + for (let i = 0; i < constructBlock.receipts.length; i++) { + if (i === 0) { + constructBlock.receipts[i].result.expectOk().expectBool(true); + } else { + constructBlock.receipts[i].result.expectOk().expectUint(i); + } + } + + const fundV2Block = passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_003); + for (let i = 0; i < fundV2Block.receipts.length; i++) { + fundV2Block.receipts[i].result.expectOk().expectUint(i + 1); + } + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked, lockPeriod)]); + for (let i = 0; i < stackingBlock.receipts.length; i++) { + stackingBlock.receipts[i].result.expectOk().expectBool(true); + } + + // progress the chain to cycle 5 + // votes are counted in cycles 2-3 + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); + + // execute two yes votes, one no vote + const votingBlock = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user2, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user3, false)]); + for (let i = 0; i < votingBlock.receipts.length; i++) { + votingBlock.receipts[i].result.expectOk().expectBool(true); + } + + // execute ccip-022 + const executeBlock = passProposal(chain, accounts, PROPOSALS.CCIP_022); + assertEquals(executeBlock.receipts.length, 3); + for (let i = 0; i < executeBlock.receipts.length; i++) { + executeBlock.receipts[i].result.expectOk().expectUint(i + 1); + } + + // get contract redemption info + + const redemptionInfo = await ccd012RedemptionNyc.getRedemptionInfo().result; + // console.log("v2 only redemptionInfo", parseClarityTuple(redemptionInfo)); + + // get user balances + const user1Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user1.address).result; + const user2Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user2.address).result; + const user3Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user3.address).result; + const user4Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user4.address).result; + + const user1InfoObject = parseClarityTuple(user1Info); + const user2InfoObject = parseClarityTuple(user2Info); + const user3InfoObject = parseClarityTuple(user3Info); + const user4InfoObject = parseClarityTuple(user4Info); + + const userInfoObjects = [user1InfoObject, user2InfoObject, user3InfoObject, user4InfoObject]; + + // act + const redeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender), ccd012RedemptionNyc.redeemNyc(user1), ccd012RedemptionNyc.redeemNyc(user2), ccd012RedemptionNyc.redeemNyc(user3), ccd012RedemptionNyc.redeemNyc(user4)]); + // console.log("v2 only redeem block", redeemBlock); + + // assert + assertEquals(redeemBlock.receipts.length, 5); + for (let i = 0; i < redeemBlock.receipts.length; i++) { + if (i === 0) { + redeemBlock.receipts[i].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_BALANCE_NOT_FOUND); + } else { + redeemBlock.receipts[i].result + .expectOk() + .expectSome() + .expectUint(userInfoObjects[i - 1].redemptionAmount); + const expectedBurnEvent = { + asset_identifier: NYC_V2_TOKEN, + sender: userInfoObjects[i - 1].address, + amount: userInfoObjects[i - 1].nycBalances.balanceV2, + }; + redeemBlock.receipts[i].events.expectFungibleTokenBurnEvent(expectedBurnEvent.amount, expectedBurnEvent.sender, expectedBurnEvent.asset_identifier); + } + } + // console.log("----------"); + // console.log("user1Info", user1InfoObject); + // console.log("user2Info", user2InfoObject); + // console.log("user3Info", user3InfoObject); + // console.log("user4Info", user4InfoObject); + // console.log("----------"); + }, +}); + +Clarinet.test({ + name: "ccd012-redemption-nyc: redeem-nyc() succeeds with both v1 and v2 tokens", + async fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const user2 = accounts.get("wallet_2")!; + const user3 = accounts.get("wallet_3")!; + const user4 = accounts.get("wallet_4")!; + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccd012RedemptionNyc = new CCD012RedemptionNyc(chain, sender); + const ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(chain, sender); + + const amountStacked = 500; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + + // initialize contracts + const constructBlock = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + for (let i = 0; i < constructBlock.receipts.length; i++) { + if (i === 0) { + constructBlock.receipts[i].result.expectOk().expectBool(true); + } else { + constructBlock.receipts[i].result.expectOk().expectUint(i); + } + } + + const fundV1Block = passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_002); + const fundV2Block = passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_003); + for (let i = 0; i < fundV1Block.receipts.length; i++) { + fundV1Block.receipts[i].result.expectOk().expectUint(i + 1); + fundV2Block.receipts[i].result.expectOk().expectUint(i + 1); + } + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked, lockPeriod)]); + for (let i = 0; i < stackingBlock.receipts.length; i++) { + stackingBlock.receipts[i].result.expectOk().expectBool(true); + } + + // progress the chain to cycle 5 + // votes are counted in cycles 2-3 + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); + + // execute two yes votes, one no vote + const votingBlock = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user2, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user3, false)]); + for (let i = 0; i < votingBlock.receipts.length; i++) { + votingBlock.receipts[i].result.expectOk().expectBool(true); + } + + // execute ccip-022 + const executeBlock = passProposal(chain, accounts, PROPOSALS.CCIP_022); + assertEquals(executeBlock.receipts.length, 3); + for (let i = 0; i < executeBlock.receipts.length; i++) { + executeBlock.receipts[i].result.expectOk().expectUint(i + 1); + } + + // get contract redemption info + + const redemptionInfo = await ccd012RedemptionNyc.getRedemptionInfo().result; + // console.log("v1 + v2 redemptionInfo", parseClarityTuple(redemptionInfo)); + + // get user balances + const user1Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user1.address).result; + const user2Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user2.address).result; + const user3Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user3.address).result; + const user4Info = await ccd012RedemptionNyc.getUserRedemptionInfo(user4.address).result; + + const user1InfoObject = parseClarityTuple(user1Info); + const user2InfoObject = parseClarityTuple(user2Info); + const user3InfoObject = parseClarityTuple(user3Info); + const user4InfoObject = parseClarityTuple(user4Info); + + const userInfoObjects = [user1InfoObject, user2InfoObject, user3InfoObject, user4InfoObject]; + + // act + const redeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender), ccd012RedemptionNyc.redeemNyc(user1), ccd012RedemptionNyc.redeemNyc(user2), ccd012RedemptionNyc.redeemNyc(user3), ccd012RedemptionNyc.redeemNyc(user4)]); + // console.log("v1 + v2 redeem block", redeemBlock); + + // assert + assertEquals(redeemBlock.receipts.length, 5); + for (let i = 0; i < redeemBlock.receipts.length; i++) { + if (i === 0) { + redeemBlock.receipts[i].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_BALANCE_NOT_FOUND); + } else { + redeemBlock.receipts[i].result + .expectOk() + .expectSome() + .expectUint(userInfoObjects[i - 1].redemptionAmount); + const expectedBurnEventV1 = { + asset_identifier: NYC_V1_TOKEN, + sender: userInfoObjects[i - 1].address, + amount: userInfoObjects[i - 1].nycBalances.balanceV1, + }; + const expectedBurnEventV2 = { + asset_identifier: NYC_V2_TOKEN, + sender: userInfoObjects[i - 1].address, + amount: userInfoObjects[i - 1].nycBalances.balanceV2, + }; + redeemBlock.receipts[i].events.expectFungibleTokenBurnEvent(expectedBurnEventV1.amount, expectedBurnEventV1.sender, expectedBurnEventV1.asset_identifier); + redeemBlock.receipts[i].events.expectFungibleTokenBurnEvent(expectedBurnEventV2.amount, expectedBurnEventV2.sender, expectedBurnEventV2.asset_identifier); + } + } + // console.log("----------"); + // console.log("user1Info", user1InfoObject); + // console.log("user2Info", user2InfoObject); + // console.log("user3Info", user3InfoObject); + // console.log("user4Info", user4InfoObject); + // console.log("----------"); + }, +}); + +type RedemptionInfo = { + blockHeight: number; + contractBalance: number; + currentContractBalance: number; + redemptionRatio: number; + redemptionsEnabled: boolean; + totalRedeemed: number; + totalSupply: number; +}; + +type UserInfo = { + address: string; + nycBalances: { + address: string; + balanceV1: number; + balanceV2: number; + totalBalance: number; + }; + redemptionAmount: number; + redemptionClaims: number; +}; + +Clarinet.test({ + name: "ccd012-redemption-nyc: redeem-nyc() succeeds with additional claims after unstacking tokens", + async fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; // 10k NYC + const user2 = accounts.get("wallet_2")!; // 1M NYC + const user3 = accounts.get("wallet_3")!; // 5M NYC + const user4 = accounts.get("wallet_4")!; // 10M NYC + const user5 = accounts.get("wallet_5")!; // 100M NYC + const user6 = accounts.get("wallet_6")!; // 1B NYC + const user7 = accounts.get("wallet_7")!; // 4B NYC + const users = [user1, user2, user3, user4, user5, user6, user7]; + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccd012RedemptionNyc = new CCD012RedemptionNyc(chain, sender); + const ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(chain, sender); + + const amountStacked = 10000; + const lockPeriod = 10; + + const redemptionDecimals = 8; + const redemptionScaleFactor = 10 ** redemptionDecimals; + const redemptionTolerance = 1e-4; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + + // initialize contracts + const constructBlock = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + for (let i = 0; i < constructBlock.receipts.length; i++) { + if (i === 0) { + constructBlock.receipts[i].result.expectOk().expectBool(true); + } else { + constructBlock.receipts[i].result.expectOk().expectUint(i); + } + } + + // fund accounts with V1 and V2 NYC + const fundV1Block = passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_002); + const fundV2Block = passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_003); + for (let i = 0; i < fundV1Block.receipts.length; i++) { + fundV1Block.receipts[i].result.expectOk().expectUint(i + 1); + fundV2Block.receipts[i].result.expectOk().expectUint(i + 1); + } + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked * 2, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked * 3, lockPeriod), ccd007CityStacking.stack(user4, nyc.cityName, amountStacked * 4, lockPeriod)]); + for (let i = 0; i < stackingBlock.receipts.length; i++) { + stackingBlock.receipts[i].result.expectOk().expectBool(true); + } + + // progress the chain to cycle 5 + // votes are counted in cycles 2-3 + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); + + // execute two yes votes, one no vote + const votingBlock = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user2, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user3, false)]); + for (let i = 0; i < votingBlock.receipts.length; i++) { + votingBlock.receipts[i].result.expectOk().expectBool(true); + } + + // execute ccip-022 + const executeBlock = passProposal(chain, accounts, PROPOSALS.CCIP_022); + assertEquals(executeBlock.receipts.length, 3); + for (let i = 0; i < executeBlock.receipts.length; i++) { + executeBlock.receipts[i].result.expectOk().expectUint(i + 1); + } + + // get contract redemption info + const redemptionInfo: RedemptionInfo = await ccd012RedemptionNyc.getRedemptionInfo().result; + const redemptionInfoObject = parseClarityTuple(redemptionInfo); + // console.log("------------------------------"); + // console.log("contract redemption info after ccip-022 execution:"); + // console.log(redemptionInfoObject); + + // calculate the redemption ratios for comparison + const redemptionRatioInContract = redemptionInfoObject.redemptionRatio / redemptionScaleFactor; + const redemptionRatioInTest = redemptionInfoObject.contractBalance / redemptionInfoObject.totalSupply; + // console.log("------------------------------"); + // console.log("contract redemption ratio after ccip-022 execution:"); + // console.log("ratio in contract: ", redemptionRatioInContract); + // console.log("ratio calc in test: ", redemptionRatioInTest); + + // check that the ratio is correctly set based on known balance and total supply + assertAlmostEquals(redemptionRatioInContract, redemptionRatioInTest, redemptionTolerance); + + // check that the contract balance is equal to first known balance minus redeemed amount by all users + assertEquals(Number(redemptionInfoObject.currentContractBalance), redemptionInfoObject.contractBalance - redemptionInfoObject.totalRedeemed); + + // get user balances from users array + const userInfoObjects: UserInfo[] = []; + for (let i = 0; i < users.length; i++) { + const userInfo = await ccd012RedemptionNyc.getUserRedemptionInfo(users[i].address).result; + const userInfoObject = parseClarityTuple(userInfo); + userInfoObjects.push(userInfoObject); + } + // console.log("------------------------------"); + // console.log("user redemption info after ccip-022 execution:"); + // userInfoObjects.map((userInfo, idx) => { + // console.log("user " + (idx + 1) + " info: ", userInfo); + // }); + + // console.log("------------------------------"); + // console.log("user redemption ratios before first redemption:"); + const redemptionRatios = userInfoObjects.map((userInfo) => { + if (userInfo.nycBalances.totalBalance > 0) { + return userInfo.redemptionAmount / userInfo.nycBalances.totalBalance; + } + return 0; + }); + for (let i = 0; i < redemptionRatios.length; i++) { + // console.log("redemption ratio user " + (i + 1) + ": ", redemptionRatios[i]); + assertAlmostEquals(redemptionRatios[i], redemptionRatioInContract, redemptionTolerance); + } + + // redeem token balances once for users 1-4 + // leave users 5-7 untouched with a large balance + const firstRedeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender), ccd012RedemptionNyc.redeemNyc(user1), ccd012RedemptionNyc.redeemNyc(user2), ccd012RedemptionNyc.redeemNyc(user3), ccd012RedemptionNyc.redeemNyc(user4)]); + // console.log("------------------------------"); + // console.log("firstRedeemBlock", firstRedeemBlock); + assertEquals(firstRedeemBlock.receipts.length, userInfoObjects.length - 2); + + for (let i = 0; i < firstRedeemBlock.receipts.length; i++) { + // expect first claim to fail with no NYC balance + if (i === 0) { + firstRedeemBlock.receipts[i].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_BALANCE_NOT_FOUND); + } else { + // claim should succeed for all other users + firstRedeemBlock.receipts[i].result + .expectOk() + .expectSome() + .expectUint(userInfoObjects[i - 1].redemptionAmount); + // if there was a v1 balance, we should see a v1 burn event + if (userInfoObjects[i - 1].nycBalances.balanceV1 > 0) { + const expectedBurnEventV1 = { + asset_identifier: NYC_V1_TOKEN, + sender: userInfoObjects[i - 1].address, + amount: userInfoObjects[i - 1].nycBalances.balanceV1, + }; + firstRedeemBlock.receipts[i].events.expectFungibleTokenBurnEvent(expectedBurnEventV1.amount, expectedBurnEventV1.sender, expectedBurnEventV1.asset_identifier); + } + // if there was a v2 balance, we should see a v2 burn event + if (userInfoObjects[i - 1].nycBalances.balanceV2 > 0) { + const expectedBurnEventV2 = { + asset_identifier: NYC_V2_TOKEN, + sender: userInfoObjects[i - 1].address, + amount: userInfoObjects[i - 1].nycBalances.balanceV2, + }; + firstRedeemBlock.receipts[i].events.expectFungibleTokenBurnEvent(expectedBurnEventV2.amount, expectedBurnEventV2.sender, expectedBurnEventV2.asset_identifier); + } + } + } + + // claim stacking rewards from cycle u10 + // progress the chain to cycle 15 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 16 + 10); + const stackingClaimBlock = chain.mineBlock([ccd007CityStacking.claimStackingReward(user1, nyc.cityName, 10), ccd007CityStacking.claimStackingReward(user2, nyc.cityName, 10), ccd007CityStacking.claimStackingReward(user3, nyc.cityName, 10), ccd007CityStacking.claimStackingReward(user4, nyc.cityName, 10)]); + // console.log("stackingClaimBlock", stackingClaimBlock); + for (let i = 0; i < stackingClaimBlock.receipts.length; i++) { + stackingClaimBlock.receipts[i].result.expectOk().expectBool(true); + } + + // get contract redemption info + const redemptionInfo2 = await ccd012RedemptionNyc.getRedemptionInfo().result; + const redemptionInfoObject2 = parseClarityTuple(redemptionInfo2); + // console.log("------------------------------"); + // console.log("contract redemption info after first redemption:"); + // console.log(redemptionInfoObject2); + + // check that the contract balance is equal to first known balance minus redeemed amount by all users + assertEquals(Number(redemptionInfoObject2.currentContractBalance), redemptionInfoObject2.contractBalance - redemptionInfoObject2.totalRedeemed); + + // get user balances from users array + const userInfoObjects2: UserInfo[] = []; + for (let i = 0; i < users.length; i++) { + const userInfo = await ccd012RedemptionNyc.getUserRedemptionInfo(users[i].address).result; + const userInfoObject = parseClarityTuple(userInfo); + userInfoObjects2.push(userInfoObject); + } + // console.log("------------------------------"); + // console.log("user redemption info after first redemption:"); + // userInfoObjects2.map((userInfo, idx) => { + // console.log("user " + (idx + 1) + " info: ", userInfo); + // }); + + // console.log("------------------------------"); + // console.log("redemption ratios before second redemption:"); + const redemptionRatios2 = userInfoObjects2.map((userInfo) => { + if (userInfo.nycBalances.totalBalance > 0) { + return userInfo.redemptionAmount / userInfo.nycBalances.totalBalance; + } + return 0; + }); + for (let i = 0; i < redemptionRatios2.length; i++) { + // console.log("redemption ratio user " + (i + 1) + ": ", redemptionRatios2[i]); + assertAlmostEquals(redemptionRatios2[i], redemptionRatioInContract, redemptionTolerance); + } + + // act + // redeem token balances once for each user + const secondRedeemBlock = chain.mineBlock([ccd012RedemptionNyc.redeemNyc(sender), ccd012RedemptionNyc.redeemNyc(user1), ccd012RedemptionNyc.redeemNyc(user2), ccd012RedemptionNyc.redeemNyc(user3), ccd012RedemptionNyc.redeemNyc(user4), ccd012RedemptionNyc.redeemNyc(user5), ccd012RedemptionNyc.redeemNyc(user6), ccd012RedemptionNyc.redeemNyc(user7)]); + // console.log("------------------------------"); + // console.log("secondRedeemBlock", secondRedeemBlock); + assertEquals(secondRedeemBlock.receipts.length, userInfoObjects2.length + 1); + + // assert + for (let i = 0; i < secondRedeemBlock.receipts.length; i++) { + // expect first claim to fail with no NYC balance + if (i === 0) { + secondRedeemBlock.receipts[i].result.expectErr().expectUint(CCD012RedemptionNyc.ErrCode.ERR_BALANCE_NOT_FOUND); + } else { + // claim should succeed for all other users + secondRedeemBlock.receipts[i].result + .expectOk() + .expectSome() + .expectUint(userInfoObjects2[i - 1].redemptionAmount); + // if there was a v1 balance, we should see a v1 burn event + if (userInfoObjects2[i - 1].nycBalances.balanceV1 > 0) { + const expectedBurnEventV1 = { + asset_identifier: NYC_V1_TOKEN, + sender: userInfoObjects2[i - 1].address, + amount: userInfoObjects2[i - 1].nycBalances.balanceV1, + }; + secondRedeemBlock.receipts[i].events.expectFungibleTokenBurnEvent(expectedBurnEventV1.amount, expectedBurnEventV1.sender, expectedBurnEventV1.asset_identifier); + } + // if there was a v2 balance, we should see a v2 burn event + if (userInfoObjects2[i - 1].nycBalances.balanceV2 > 0) { + const expectedBurnEventV2 = { + asset_identifier: NYC_V2_TOKEN, + sender: userInfoObjects2[i - 1].address, + amount: userInfoObjects2[i - 1].nycBalances.balanceV2, + }; + secondRedeemBlock.receipts[i].events.expectFungibleTokenBurnEvent(expectedBurnEventV2.amount, expectedBurnEventV2.sender, expectedBurnEventV2.asset_identifier); + } + } + } + + // get the redemption info from the contract for analysis + const redemptionInfo3 = await ccd012RedemptionNyc.getRedemptionInfo().result; + const redemptionInfoObject3 = parseClarityTuple(redemptionInfo3); + + // console.log("------------------------------"); + // console.log("contract redemption info after second redemption:"); + // console.log(redemptionInfoObject3); + + // check that the contract balance is equal to first known balance minus redeemed amount by all users + assertEquals(Number(redemptionInfoObject3.currentContractBalance), redemptionInfoObject3.contractBalance - redemptionInfoObject3.totalRedeemed); + + // get user balances from users array + const userInfoObjects3: UserInfo[] = []; + for (let i = 0; i < users.length; i++) { + const userInfo = await ccd012RedemptionNyc.getUserRedemptionInfo(users[i].address).result; + const userInfoObject = parseClarityTuple(userInfo); + userInfoObjects3.push(userInfoObject); + } + // console.log("------------------------------"); + // console.log("user redemption info after second redemption:"); + // userInfoObjects3.map((userInfo, idx) => { + // console.log("user " + (idx + 1) + " info: ", userInfo); + // }); + }, +}); diff --git a/tests/proposals/ccip022-treasury-redemption-nyc.test.ts b/tests/proposals/ccip022-treasury-redemption-nyc.test.ts new file mode 100644 index 00000000..00088529 --- /dev/null +++ b/tests/proposals/ccip022-treasury-redemption-nyc.test.ts @@ -0,0 +1,667 @@ +import { Account, Clarinet, Chain, types } from "../../utils/deps.ts"; +import { constructAndPassProposal, passProposal, PROPOSALS, nyc } from "../../utils/common.ts"; +import { CCD006CityMining } from "../../models/extensions/ccd006-citycoin-mining.model.ts"; +import { CCD007CityStacking } from "../../models/extensions/ccd007-citycoin-stacking.model.ts"; +import { CCIP022TreasuryRedemptionNYC } from "../../models/proposals/ccip022-treasury-redemption-nyc.model.ts"; + +// helper function to print voting data for users 1, 2, and 3 +function printVotingData(ccd007: CCD007CityStacking, ccip022: CCIP022TreasuryRedemptionNYC) { + console.log("contract vote totals nyc:"); + console.log(JSON.stringify(ccip022.getVoteTotalNyc(), null, 2)); + console.log("contract vote totals:"); + console.log(JSON.stringify(ccip022.getVoteTotals(), null, 2)); + + console.log("user 1:"); + console.log(ccip022.getVoterInfo(1)); + console.log("user 1 NYC:"); + console.log(ccd007.getStacker(nyc.cityId, 2, 1)); + console.log(ccip022.getNycVote(1, false)); + console.log(ccip022.getNycVote(1, true)); + + console.log("user 2:"); + console.log(ccip022.getVoterInfo(2)); + console.log("user 2 NYC:"); + console.log(ccd007.getStacker(nyc.cityId, 2, 2)); + console.log(ccip022.getNycVote(2, false)); + console.log(ccip022.getNycVote(2, true)); + + console.log("user 3:"); + console.log(ccip022.getVoterInfo(3)); + console.log("user 3 NYC:"); + console.log(ccd007.getStacker(nyc.cityId, 2, 3)); + console.log(ccip022.getNycVote(3, false)); + console.log(ccip022.getNycVote(3, true)); +} + +Clarinet.test({ + name: "ccip-022: execute() fails with ERR_VOTE_FAILED if there are no votes", + fn(chain: Chain, accounts: Map) { + // arrange + + // initialize contracts + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + + // act + + // execute ccip-022 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_022); + + // assert + block.receipts[2].result.expectErr().expectUint(CCIP022TreasuryRedemptionNYC.ErrCode.ERR_VOTE_FAILED); + }, +}); + +Clarinet.test({ + name: "ccip-022: execute() fails with ERR_VOTE_FAILED if there are more no than yes votes", + fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const user2 = accounts.get("wallet_2")!; + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(chain, sender); + + const amountStacked = 500; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + + // initialize contracts + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_003); + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod)]); + // make sure every transaction succeeded + for (let i = 0; i < stackingBlock.receipts.length; i++) { + stackingBlock.receipts[i].result.expectOk().expectBool(true); + } + + // progress the chain to cycle 5 + // votes are counted in cycles 2-3 + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); + + // act + + // execute two no votes + const votingBlock = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, false), ccip022TreasuryRedemptionNyc.voteOnProposal(user2, false)]); + for (let i = 0; i < votingBlock.receipts.length; i++) { + votingBlock.receipts[i].result.expectOk().expectBool(true); + } + + /* double check voting data + console.log(`voting block:\n${JSON.stringify(votingBlock, null, 2)}`); + printVotingData(ccd007CityStacking, ccip022TreasuryRedemptionNyc); + */ + + // execute ccip-022 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_022); + + // assert + block.receipts[2].result.expectErr().expectUint(CCIP022TreasuryRedemptionNYC.ErrCode.ERR_VOTE_FAILED); + }, +}); + +Clarinet.test({ + name: "ccip-022: execute() succeeds if there is a single yes vote", + fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(chain, sender); + + const blocksMined = 10; + const amountPerBlock = 25000000; + const amountStacked = 500; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + + // initialize contracts + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_003); + + // mine to put some funds in the treasury + const miningEntries = Array.from({ length: blocksMined }, () => amountPerBlock); + const miningBlock = chain.mineBlock([ccd006CityMiningV2.mine(sender, nyc.cityName, miningEntries)]); + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod)]); + stackingBlock.receipts[0].result.expectOk().expectBool(true); + + // progress the chain to cycle 5 + // votes are counted in cycles 2-3 + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); + + // blocks to claim is an array of block heights + // starting with the miningBlock height + // and ending after blocksMined blocks + const blocksToClaim = Array.from({ length: blocksMined }, (_, i) => miningBlock.height + i - 1); + + // claim mined blocks to increase total supply + const claimBlock = chain.mineBlock(blocksToClaim.map((height) => ccd006CityMiningV2.claimMiningReward(sender, nyc.cityName, height))); + for (let i = 0; i < claimBlock.receipts.length; i++) { + claimBlock.receipts[i].result.expectOk().expectBool(true); + } + + // act + + // execute two yes votes with MIA only + const votingBlock = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, true)]); + votingBlock.receipts[0].result.expectOk().expectBool(true); + + /* double check voting data + console.log(`voting block:\n${JSON.stringify(votingBlock, null, 2)}`); + printVotingData(ccd007CityStacking, ccip022TreasuryRedemptionNyc); + */ + + // execute ccip-022 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_022); + + // assert + block.receipts[2].result.expectOk().expectUint(3); + }, +}); + +Clarinet.test({ + name: "ccip-022: execute() succeeds if there are more yes than no votes", + fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const user2 = accounts.get("wallet_2")!; + const user3 = accounts.get("wallet_3")!; + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(chain, sender); + + const blocksMined = 10; + const amountPerBlock = 25000000; + const amountStacked = 500; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + + // initialize contracts + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_003); + + // mine to put some funds in the treasury + const miningEntries = Array.from({ length: blocksMined }, () => amountPerBlock); + const miningBlock = chain.mineBlock([ccd006CityMiningV2.mine(sender, nyc.cityName, miningEntries)]); + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked, lockPeriod)]); + // for length of mineBlock array, expectOk and expectBool(true) + for (let i = 0; i < stackingBlock.receipts.length; i++) { + stackingBlock.receipts[i].result.expectOk().expectBool(true); + } + + // progress the chain to cycle 5 + // votes are counted in cycles 2-3 + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); + + // blocks to claim is an array of block heights + // starting with the miningBlock height + // and ending after blocksMined blocks + const blocksToClaim = Array.from({ length: blocksMined }, (_, i) => miningBlock.height + i - 1); + + // claim mined blocks to increase total supply + const claimBlock = chain.mineBlock(blocksToClaim.map((height) => ccd006CityMiningV2.claimMiningReward(sender, nyc.cityName, height))); + for (let i = 0; i < claimBlock.receipts.length; i++) { + claimBlock.receipts[i].result.expectOk().expectBool(true); + } + + // act + + // execute two yes votes, one no vote + const votingBlock = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user2, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user3, false)]); + for (let i = 0; i < votingBlock.receipts.length; i++) { + votingBlock.receipts[i].result.expectOk().expectBool(true); + } + + // double check voting data + // console.log(`voting block:\n${JSON.stringify(votingBlock, null, 2)}`); + // printVotingData(ccd007CityStacking, ccip022TreasuryRedemptionNyc); + + // execute ccip-022 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_022); + + // assert + block.receipts[2].result.expectOk().expectUint(3); + }, +}); + +Clarinet.test({ + name: "ccip-022: execute() succeeds if there are more yes than no votes after a reversal", + fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const user2 = accounts.get("wallet_2")!; + const user3 = accounts.get("wallet_3")!; + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(chain, sender); + + const blocksMined = 10; + const amountPerBlock = 25000000; + const amountStacked = 500; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + + // initialize contracts + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_003); + + // mine to put some funds in the treasury + const miningEntries = Array.from({ length: blocksMined }, () => amountPerBlock); + const miningBlock = chain.mineBlock([ccd006CityMiningV2.mine(sender, nyc.cityName, miningEntries)]); + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked, lockPeriod)]); + for (let i = 0; i < stackingBlock.receipts.length; i++) { + stackingBlock.receipts[i].result.expectOk().expectBool(true); + } + + // progress the chain to cycle 5 + // votes are counted in cycles 2-3 + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); + + // blocks to claim is an array of block heights + // starting with the miningBlock height + // and ending after blocksMined blocks + const blocksToClaim = Array.from({ length: blocksMined }, (_, i) => miningBlock.height + i - 1); + + // claim mined blocks to increase total supply + const claimBlock = chain.mineBlock(blocksToClaim.map((height) => ccd006CityMiningV2.claimMiningReward(sender, nyc.cityName, height))); + for (let i = 0; i < claimBlock.receipts.length; i++) { + claimBlock.receipts[i].result.expectOk().expectBool(true); + } + + // act + + // execute two yes votes, one no vote + const votingBlock = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user2, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user3, false)]); + for (let i = 0; i < votingBlock.receipts.length; i++) { + votingBlock.receipts[i].result.expectOk().expectBool(true); + } + + /* double check voting data + console.log("BEFORE REVERSAL"); + console.log(`voting block:\n${JSON.stringify(votingBlock, null, 2)}`); + printVotingData(ccd007CityStacking, ccip022TreasuryRedemptionNyc); + */ + + const votingBlockReversed = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user3, true)]); + votingBlockReversed.receipts[0].result.expectOk().expectBool(true); + + /* double check voting data + console.log("AFTER REVERSAL"); + console.log(`voting block reversed:\n${JSON.stringify(votingBlockReversed, null, 2)}`); + printVotingData(ccd007CityStacking, ccip022TreasuryRedemptionNyc); + */ + + // execute ccip-022 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_022); + + // assert + block.receipts[2].result.expectOk().expectUint(3); + }, +}); + +Clarinet.test({ + name: "ccip-022: vote-on-proposal() fails with ERR_USER_NOT_FOUND if user is not registered in ccd003-user-registry", + fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const user2 = accounts.get("wallet_2")!; + const user3 = accounts.get("wallet_3")!; + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(chain, sender); + + const miningEntries = [25000000, 25000000]; + const amountStacked = 500; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + + // prepare for CCIP (sets up cities, tokens, and data) + const constructBlock = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + constructBlock.receipts[0].result.expectOk().expectBool(true); + passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_003); + + // mine to put funds in the mining treasury + const miningBlock = chain.mineBlock([ccd006CityMiningV2.mine(user1, nyc.cityName, miningEntries), ccd006CityMiningV2.mine(user2, nyc.cityName, miningEntries)]); + for (let i = 0; i < miningBlock.receipts.length; i++) { + miningBlock.receipts[i].result.expectOk().expectBool(true); + } + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked / 2, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked / 2, lockPeriod)]); + for (let i = 0; i < stackingBlock.receipts.length; i++) { + stackingBlock.receipts[i].result.expectOk().expectBool(true); + } + + // progress the chain to cycle 5 + // votes are counted in cycles 2-3 + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); + + // act + const votingBlock = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user2, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user3, true)]); + + // assert + votingBlock.receipts[0].result.expectOk().expectBool(true); + votingBlock.receipts[1].result.expectOk().expectBool(true); + votingBlock.receipts[2].result.expectErr().expectUint(CCIP022TreasuryRedemptionNYC.ErrCode.ERR_USER_NOT_FOUND); + }, +}); + +Clarinet.test({ + name: "ccip-022: vote-on-proposal() fails with ERR_PROPOSAL_NOT_ACTIVE if called after the vote ends", + fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const user2 = accounts.get("wallet_2")!; + const user3 = accounts.get("wallet_3"); + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(chain, sender); + + const miningEntries = [25000000, 25000000]; + const amountStacked = 500; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + + // prepare for CCIP (sets up cities, tokens, and data) + const constructBlock = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + constructBlock.receipts[0].result.expectOk().expectBool(true); + passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_003); + + // mine to put funds in the mining treasury + const miningBlock = chain.mineBlock([ccd006CityMiningV2.mine(user1, nyc.cityName, miningEntries), ccd006CityMiningV2.mine(user2, nyc.cityName, miningEntries)]); + for (let i = 0; i < miningBlock.receipts.length; i++) { + miningBlock.receipts[i].result.expectOk().expectBool(true); + } + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked, lockPeriod)]); + for (let i = 0; i < stackingBlock.receipts.length; i++) { + stackingBlock.receipts[i].result.expectOk().expectBool(true); + } + + // progress the chain to cycle 5 + // votes are counted in cycles 2-3 + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); + + // execute yes and no vote + // user 1 and 2 vote yes + // user 3 votes no + const votingBlock = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user2, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user3, false)]); + for (let i = 0; i < votingBlock.receipts.length; i++) { + votingBlock.receipts[i].result.expectOk().expectBool(true); + } + + // execute ccip-022, ending the vote + passProposal(chain, accounts, PROPOSALS.CCIP_022); + + // act + // user 1 tries to reverse their vote + const votingBlockAfter = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, false)]); + + // assert + votingBlockAfter.receipts[0].result.expectErr().expectUint(CCIP022TreasuryRedemptionNYC.ErrCode.ERR_PROPOSAL_NOT_ACTIVE); + }, +}); + +Clarinet.test({ + name: "ccip-022: vote-on-proposal() fails with ERR_VOTED_ALREADY if user already voted with the same value", + fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const user2 = accounts.get("wallet_2")!; + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(chain, sender); + + const miningEntries = [25000000, 25000000]; + const amountStacked = 500; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + + // prepare for CCIP (sets up cities, tokens, and data) + const constructBlock = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + constructBlock.receipts[0].result.expectOk().expectBool(true); + passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_003); + + // mine to put funds in the mining treasury + const miningBlock = chain.mineBlock([ccd006CityMiningV2.mine(user1, nyc.cityName, miningEntries), ccd006CityMiningV2.mine(user2, nyc.cityName, miningEntries)]); + for (let i = 0; i < miningBlock.receipts.length; i++) { + miningBlock.receipts[i].result.expectOk().expectBool(true); + } + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked / 2, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked / 2, lockPeriod)]); + for (let i = 0; i < stackingBlock.receipts.length; i++) { + stackingBlock.receipts[i].result.expectOk().expectBool(true); + } + + // progress the chain to cycle 5 + // votes are counted in cycles 2-3 + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); + + // vote yes + const votingBlock = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, true)]); + votingBlock.receipts[0].result.expectOk().expectBool(true); + + // act + const votingBlockDupe = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, true)]); + + // assert + votingBlockDupe.receipts[0].result.expectErr().expectUint(CCIP022TreasuryRedemptionNYC.ErrCode.ERR_VOTED_ALREADY); + }, +}); + +Clarinet.test({ + name: "ccip-022: read-only functions return expected values before/after reversal", + fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const user2 = accounts.get("wallet_2")!; + const user3 = accounts.get("wallet_3")!; + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip022TreasuryRedemptionNyc = new CCIP022TreasuryRedemptionNYC(chain, sender); + + const blocksMined = 10; + const amountPerBlock = 25000000; + const amountStacked = 500; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + + // prepare for CCIP (sets up cities, tokens, and data) + const constructBlock = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCIP022_TREASURY_REDEMPTION_NYC_003); + constructBlock.receipts[0].result.expectOk().expectBool(true); + + // mine to put some funds in the treasury + const miningEntries = Array.from({ length: blocksMined }, () => amountPerBlock); + const miningBlock = chain.mineBlock([ccd006CityMiningV2.mine(user1, nyc.cityName, miningEntries)]); + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user3, nyc.cityName, amountStacked, lockPeriod)]); + for (let i = 0; i < stackingBlock.receipts.length; i++) { + stackingBlock.receipts[i].result.expectOk().expectBool(true); + } + + // progress the chain to cycle 5 + // votes are counted in cycles 2-3 + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); + + // blocks to claim is an array of block heights + // starting with the miningBlock height + // and ending after blocksMined blocks + const blocksToClaim = Array.from({ length: blocksMined }, (_, i) => miningBlock.height + i - 1); + + // claim mined blocks to increase total supply + const claimBlock = chain.mineBlock(blocksToClaim.map((height) => ccd006CityMiningV2.claimMiningReward(user1, nyc.cityName, height))); + for (let i = 0; i < claimBlock.receipts.length; i++) { + claimBlock.receipts[i].result.expectOk().expectBool(true); + } + + // act + + // execute two yes votes, one no vote + const votingBlock = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user1, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user2, true), ccip022TreasuryRedemptionNyc.voteOnProposal(user3, false)]); + for (let i = 0; i < votingBlock.receipts.length; i++) { + votingBlock.receipts[i].result.expectOk().expectBool(true); + } + + // double check voting data + // console.log("BEFORE REVERSAL"); + // console.log(`voting block:\n${JSON.stringify(votingBlock, null, 2)}`); + // printVotingData(ccd007CityStacking, ccip022TreasuryRedemptionNyc); + + // vote totals NYC + ccip022TreasuryRedemptionNyc + .getVoteTotalNyc() + .result.expectSome() + .expectTuple({ totalAmountNo: types.uint(0), totalAmountYes: types.uint(1500), totalVotesNo: types.uint(0), totalVotesYes: types.uint(3) }); + + // vote totals in contract (MIA+NYC+Totals) + ccip022TreasuryRedemptionNyc + .getVoteTotals() + .result.expectSome() + .expectTuple({ nyc: { totalAmountNo: types.uint(0), totalAmountYes: types.uint(1500), totalVotesNo: types.uint(0), totalVotesYes: types.uint(3) }, totals: { totalAmountNo: types.uint(0), totalAmountYes: types.uint(2835), totalVotesNo: types.uint(0), totalVotesYes: types.uint(6) } }); + + // user 1 stats + ccip022TreasuryRedemptionNyc + .getVoterInfo(1) + .result.expectSome() + .expectTuple({ nyc: types.uint(500), vote: types.bool(true) }); + ccd007CityStacking.getStacker(nyc.cityId, 2, 1).result.expectTuple({ claimable: types.uint(0), stacked: types.uint(500) }); + ccip022TreasuryRedemptionNyc.getNycVote(1, false).result.expectSome().expectUint(500); + + // user 2 stats + ccip022TreasuryRedemptionNyc + .getVoterInfo(2) + .result.expectSome() + .expectTuple({ nyc: types.uint(500), vote: types.bool(true) }); + ccd007CityStacking.getStacker(nyc.cityId, 2, 2).result.expectTuple({ claimable: types.uint(0), stacked: types.uint(500) }); + ccip022TreasuryRedemptionNyc.getNycVote(2, false).result.expectSome().expectUint(500); + + // user 3 stats + ccip022TreasuryRedemptionNyc + .getVoterInfo(3) + .result.expectSome() + .expectTuple({ nyc: types.uint(500), vote: types.bool(false) }); + ccd007CityStacking.getStacker(nyc.cityId, 2, 3).result.expectTuple({ claimable: types.uint(0), stacked: types.uint(500) }); + ccip022TreasuryRedemptionNyc.getNycVote(3, false).result.expectSome().expectUint(500); + + // reverse the vote for user 3 + const votingBlockReversed = chain.mineBlock([ccip022TreasuryRedemptionNyc.voteOnProposal(user3, true)]); + votingBlockReversed.receipts[0].result.expectOk().expectBool(true); + + // vote totals NYC + ccip022TreasuryRedemptionNyc + .getVoteTotalNyc() + .result.expectSome() + .expectTuple({ totalAmountNo: types.uint(0), totalAmountYes: types.uint(1500), totalVotesNo: types.uint(0), totalVotesYes: types.uint(3) }); + // vote totals in contract (MIA+NYC+Totals) + ccip022TreasuryRedemptionNyc + .getVoteTotals() + .result.expectSome() + .expectTuple({ nyc: { totalAmountNo: types.uint(0), totalAmountYes: types.uint(1500), totalVotesNo: types.uint(0), totalVotesYes: types.uint(3) }, totals: { totalAmountNo: types.uint(0), totalAmountYes: types.uint(2835), totalVotesNo: types.uint(0), totalVotesYes: types.uint(6) } }); + + // user 1 stats + ccip022TreasuryRedemptionNyc + .getVoterInfo(1) + .result.expectSome() + .expectTuple({ nyc: types.uint(500), vote: types.bool(true) }); + ccd007CityStacking.getStacker(nyc.cityId, 2, 1).result.expectTuple({ claimable: types.uint(0), stacked: types.uint(500) }); + ccip022TreasuryRedemptionNyc.getNycVote(1, false).result.expectSome().expectUint(500); + + // user 2 stats + ccip022TreasuryRedemptionNyc + .getVoterInfo(2) + .result.expectSome() + .expectTuple({ nyc: types.uint(500), vote: types.bool(true) }); + ccd007CityStacking.getStacker(nyc.cityId, 2, 2).result.expectTuple({ claimable: types.uint(0), stacked: types.uint(500) }); + ccip022TreasuryRedemptionNyc.getNycVote(2, false).result.expectSome().expectUint(500); + + // user 3 stats + ccip022TreasuryRedemptionNyc + .getVoterInfo(3) + .result.expectSome() + .expectTuple({ nyc: types.uint(500), vote: types.bool(true) }); + ccd007CityStacking.getStacker(nyc.cityId, 2, 3).result.expectTuple({ claimable: types.uint(0), stacked: types.uint(500) }); + ccip022TreasuryRedemptionNyc.getNycVote(3, false).result.expectSome().expectUint(500); + + // double check voting data + //console.log("AFTER REVERSAL"); + //console.log(`voting block reversed:\n${JSON.stringify(votingBlockReversed, null, 2)}`); + //printVotingData(ccd007CityStacking, ccip022TreasuryRedemptionNyc); + + // execute ccip-022 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_022); + + // assert + block.receipts[2].result.expectOk().expectUint(3); + }, +}); + +/* +Clarinet.test({ + name: "", + fn(chain: Chain, accounts: Map) { + // arrange + + // act + + // assert + } +}) +*/ diff --git a/utils/common.ts b/utils/common.ts index 09a8b68e..e302729c 100644 --- a/utils/common.ts +++ b/utils/common.ts @@ -32,6 +32,7 @@ export const EXTENSIONS = { CCD008_CITY_ACTIVATION: ADDRESS.concat(".ccd008-city-activation"), CCD009_AUTH_V2_ADAPTER: ADDRESS.concat(".ccd009-auth-v2-adapter"), CCD010_CORE_V2_ADAPTER: ADDRESS.concat(".ccd010-core-v2-adapter"), + CCD012_REDEMPTION_NYC: ADDRESS.concat(".ccd012-redemption-nyc"), }; export const PROPOSALS = { @@ -42,6 +43,7 @@ export const PROPOSALS = { CCIP_017: ADDRESS.concat(".ccip017-extend-sunset-period"), CCIP_020: ADDRESS.concat(".ccip020-graceful-protocol-shutdown"), CCIP_021: ADDRESS.concat(".ccip021-extend-sunset-period-2"), + CCIP_022: ADDRESS.concat(".ccip022-treasury-redemption-nyc"), TEST_CCD001_DIRECT_EXECUTE_001: ADDRESS.concat(".test-ccd001-direct-execute-001"), TEST_CCD001_DIRECT_EXECUTE_002: ADDRESS.concat(".test-ccd001-direct-execute-002"), TEST_CCD001_DIRECT_EXECUTE_003: ADDRESS.concat(".test-ccd001-direct-execute-003"), @@ -119,11 +121,17 @@ export const PROPOSALS = { TEST_CCIP014_POX3_001: ADDRESS.concat(".test-ccip014-pox-3-001"), TEST_CCIP014_POX3_002: ADDRESS.concat(".test-ccip014-pox-3-002"), TEST_CCIP020_GRACEFUL_PROTOCOL_SHUTDOWN_001: ADDRESS.concat(".test-ccip020-shutdown-001"), + TEST_CCIP022_TREASURY_REDEMPTION_NYC_001: ADDRESS.concat(".test-ccip022-treasury-redemption-nyc-001"), + TEST_CCIP022_TREASURY_REDEMPTION_NYC_002: ADDRESS.concat(".test-ccip022-treasury-redemption-nyc-002"), + TEST_CCIP022_TREASURY_REDEMPTION_NYC_003: ADDRESS.concat(".test-ccip022-treasury-redemption-nyc-003"), + TEST_CCIP022_TREASURY_REDEMPTION_NYC_004: ADDRESS.concat(".test-ccip022-treasury-redemption-nyc-004"), + TEST_CCIP022_TREASURY_REDEMPTION_NYC_005: ADDRESS.concat(".test-ccip022-treasury-redemption-nyc-005"), }; export const EXTERNAL = { FT_MIA: ADDRESS.concat(".test-ccext-governance-token-mia"), FT_NYC: ADDRESS.concat(".test-ccext-governance-token-nyc"), + FT_NYC_V1: ADDRESS.concat(".test-ccext-governance-token-nyc-v1"), NFT_MIA: ADDRESS.concat(".test-ccext-nft-mia"), NFT_NYC: ADDRESS.concat(".test-ccext-nft-nyc"), }; @@ -236,3 +244,21 @@ export const nyc: CityData = { treasuryV2Id: 2, treasuryV2Name: "mining-v2", }; + +// parses an (ok ...) response into a JS object +export function parseClarityTuple(clarityString) { + // Step 1: Remove the outer (ok ) and the closing parenthesis + let jsonString = clarityString.replace("(ok ", "").replace(")", ""); + + // Step 2: Add quotes around keys + jsonString = jsonString.replace(/([a-zA-Z0-9_]+):/g, '"$1":'); + + // Step 3: Add quotes around string values (addresses) + jsonString = jsonString.replace(/: ([a-zA-Z0-9_]+)/g, ': "$1"'); + + // Step 4: Remove 'u' prefix from integers + jsonString = jsonString.replace(/u([0-9]+)/g, "$1"); + + // Parse the JSON string to object + return JSON.parse(jsonString); +} diff --git a/utils/deps.ts b/utils/deps.ts index 70c5fc41..5985f484 100644 --- a/utils/deps.ts +++ b/utils/deps.ts @@ -1,5 +1,5 @@ -export type { Account, Block, ReadOnlyFn, TxReceipt } from "https://deno.land/x/clarinet@v1.7.1/index.ts"; +export type { Account, Block, ReadOnlyFn, TxReceipt } from "https://deno.land/x/clarinet@v1.8.0/index.ts"; -export { Clarinet, Chain, Tx, types } from "https://deno.land/x/clarinet@v1.7.1/index.ts"; +export { Clarinet, Chain, Tx, types } from "https://deno.land/x/clarinet@v1.8.0/index.ts"; -export { assertEquals, assert } from "https://deno.land/std@0.201.0/testing/asserts.ts"; +export { assertAlmostEquals, assertEquals, assert } from "https://deno.land/std@0.201.0/testing/asserts.ts";