Source Code


;; ---------------------------------------------------------
;; MEMEGOAT X CATDOG LOCK
;; ---------------------------------------------------------

(use-trait ft-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait)
(use-trait ft-trait-ext 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.trait-sip-010.sip-010-trait)

;; ERRS
(define-constant ERR-NOT-AUTHORIZED (err u1000))
(define-constant ERR-INITIALIZED (err u1001))
(define-constant ERR-NOT-INITIALIZED (err u1001))
(define-constant ERR-INVALID-BLOCK (err u5000))
(define-constant ERR-INVALID-BLOCK-ARR (err u5002))
(define-constant ERR-INSUFFICIENT_AMOUNT (err u5001))
(define-constant ERR-INVALID-TOKEN (err u5005))
(define-constant ERR-INVALID-AMOUNT (err u6001))
(define-constant ERR-INVALID-LOCK (err u6003))
(define-constant ERR-OUT-OF-BOUNDS (err u6005))
(define-constant ERR-ONLY-SINGLE-LOCK (err u7001))
(define-constant ERR-INFO-NOT-FOUND (err u7006))
(define-constant ERR-REWARD-BLOCK-NOT-REACHED (err u7007))
(define-constant ERR-CANNOT-EXCEED-CLAIM-AMOUNT (err u7008))
(define-constant ERR-INVALID-PERCENTAGE (err u8000))
(define-constant ERR-ALREADY-UNLOCKED (err u8002))
(define-constant ERR-ALREADY-CLAIMED (err u8003))
(define-constant ERR-INVALID-CONFIG (err u8004))

;; STORAGE
(define-constant LOCK-TOKEN 'SP3ATFW5VSD0W4N0E3K1E4CGFE8MJXQ9XFFMQ0HBY.catdog-stxcity)
(define-constant LOCK-ADDRESS (as-contract tx-sender))
(define-data-var initialized bool false)
(define-data-var lock-owner principal tx-sender)
(define-data-var lock-block uint u0)
(define-data-var total-amount uint u0)
(define-data-var is-vested bool false)
(define-data-var unlock-blocks (list 200 {height: uint, percentage: uint}) (list {height: u0, percentage: u0}))
(define-data-var total-addresses uint u0)

(define-map user-lock-data
    { address: principal }
    {
      last-claim-block: uint,
      claim-index: uint, 
      total-claimed: uint,
      total-amount: uint,
      withdrawal-address: principal,
    }
)

;; define the fee parameters
(define-constant STX-FEE u1000000) ;; small stacks fee to prevent spams
(define-constant SEC-FEE-TOKEN 'SP2F4QC563WN0A0949WPH5W1YXVC4M1R46QKE0G14.memegoatstx) ;; in this case memegoat
(define-constant SEC-TOKEN-FEE u1000000000) ;; option memegoat ~ 1000 memegoat


(define-read-only (get-token-lock-data)
  (ok {
    initialized: (var-get initialized),
    lock-block: (var-get lock-block),
    total-amount: (var-get total-amount),
    lock-owner: (var-get lock-owner),
    locked-token: LOCK-TOKEN,
    is-vested: (var-get is-vested),
    unlock-blocks: (var-get unlock-blocks),
    total-addresses: (var-get total-addresses)
  })
)

(define-read-only (get-user-lock-info (address principal))
  (ok (unwrap! (map-get? user-lock-data {address: address}) ERR-INFO-NOT-FOUND))
)

(define-read-only (is-dao-or-extension)
  (ok (asserts! (or (is-eq tx-sender 'SP2F4QC563WN0A0949WPH5W1YXVC4M1R46QKE0G14.memegoat-community-dao) (contract-call? 'SP2F4QC563WN0A0949WPH5W1YXVC4M1R46QKE0G14.memegoat-community-dao is-extension contract-caller)) ERR-NOT-AUTHORIZED))
)

;; DAO ACTION
(define-public (emergency-withdraw (token-trait <ft-trait>) (recipient principal))
  (let
    (
      (remaining-bal (try! (contract-call? token-trait get-balance LOCK-ADDRESS)))
    )
    (try! (is-dao-or-extension))
    (as-contract (contract-call? token-trait transfer remaining-bal tx-sender recipient none))
  )
)

;; PUBLIC CALLS
;; relockToken
(define-public (relock-token (new-unlock-block uint) (fee-in-stx bool) (secondary-token-trait <ft-trait-ext>) ) 
  (begin 
    (try! (check-is-owner))
    (try! (check-is-initialized))
    (let
      (
        (sender tx-sender)
        (vested (var-get is-vested))
        (user-lock-info (try! (get-user-lock-info sender)))
        (claim-index (get claim-index user-lock-info))
        (total-claimed (get total-claimed user-lock-info))
        (lock-block- (var-get lock-block))
        (unlock-blocks-data (var-get unlock-blocks))
        (user-unlock-block (unwrap! (element-at? unlock-blocks-data claim-index) ERR-OUT-OF-BOUNDS))
        (updated-block-info (merge user-unlock-block {
          height: new-unlock-block
        }))
        (updated-unlock-blocks (unwrap! (replace-at? unlock-blocks-data claim-index updated-block-info) ERR-OUT-OF-BOUNDS)) 
      )
      (asserts! (not vested) ERR-ONLY-SINGLE-LOCK)
      (asserts! (and (> new-unlock-block lock-block-) (> new-unlock-block block-height)) ERR-INVALID-BLOCK)
      (asserts! (is-eq total-claimed u0) ERR-ALREADY-CLAIMED)
      (asserts! (> (get height user-unlock-block) block-height) ERR-ALREADY-UNLOCKED)

      (if fee-in-stx
        ;; Pay fee in STX
        (try! (stx-transfer? STX-FEE tx-sender 'SP2F4QC563WN0A0949WPH5W1YXVC4M1R46QKE0G14.memegoat-treasury))
        ;; Burn token
        (begin
          (asserts! (is-eq SEC-FEE-TOKEN (contract-of secondary-token-trait)) ERR-INVALID-TOKEN)
          (try! (contract-call? secondary-token-trait burn SEC-TOKEN-FEE sender))
        )
      )
      (var-set unlock-blocks updated-unlock-blocks)
    )
    (ok true)
  )
)

;; withdraw
(define-public (withdraw-token (locked-token <ft-trait>)) 
  (begin
    (try! (check-is-initialized))
    (let
      (
        (recipient tx-sender)
        (vested (var-get is-vested))
        (user-lock-info (try! (get-user-lock-info recipient)))
        (unlock-blocks-data (var-get unlock-blocks))
        (claim-index (get claim-index user-lock-info))
        (locked-amount (get total-amount user-lock-info))
        (total-claimed (get total-claimed user-lock-info))
        (withdrawal-address (get withdrawal-address user-lock-info))
        (user-unlock-block (unwrap! (element-at? unlock-blocks-data claim-index) ERR-OUT-OF-BOUNDS))
        (height (get height user-unlock-block))
        (percentage (get percentage user-unlock-block))
        (unlock-amount (/ (* locked-amount percentage) u100))
        (user-lock-info-updated (merge user-lock-info {
            claim-index: (+ claim-index u1),
            last-claim-block: block-height,
            total-claimed: (+ total-claimed unlock-amount)
          })
        )
      )
  
      (asserts! (> block-height height) ERR-REWARD-BLOCK-NOT-REACHED)
      (asserts! (is-eq LOCK-TOKEN (contract-of locked-token)) ERR-INVALID-TOKEN)
      (asserts! (< total-claimed locked-amount) ERR-CANNOT-EXCEED-CLAIM-AMOUNT)
      (asserts! (is-eq (contract-of locked-token) LOCK-TOKEN) ERR-INVALID-TOKEN)

      ;; transfer token from vault
      (try! (as-contract (contract-call? locked-token transfer unlock-amount tx-sender recipient none)))
      (map-set user-lock-data {address: recipient} user-lock-info-updated)
      
      (if (is-eq claim-index (- (len unlock-blocks-data) u1))
        (map-delete user-lock-data {address: recipient})
        true
      )
    )
    (ok true)
  )
)

;; incrementlock
(define-public (increment-lock (locked-token <ft-trait>) (amount uint)) 
  (begin 
    (try! (check-is-owner))
    (try! (check-is-initialized))
    (asserts! (> amount u0) ERR-INVALID-AMOUNT)
    (let
      (
        (sender tx-sender)
        (vested (var-get is-vested))
        (unlock-blocks-data (var-get unlock-blocks))
        (total-locked (var-get total-amount))
        (user-lock-info (try! (get-user-lock-info sender)))
        (claim-index (get claim-index user-lock-info))
        (user-lock-amount (get total-amount user-lock-info))
        (user-unlock-block (unwrap! (element-at? unlock-blocks-data claim-index) ERR-OUT-OF-BOUNDS))
        (user-lock-info-updated (merge user-lock-info {
          total-amount: (+ user-lock-amount amount)
        }))
      ) 
      (asserts! (not vested) ERR-ONLY-SINGLE-LOCK)
      (asserts! (is-eq LOCK-TOKEN (contract-of locked-token)) ERR-INVALID-TOKEN)
      (asserts! (> (get height user-unlock-block) block-height) ERR-ALREADY-UNLOCKED)

      ;; transfer token to vault
      (try! (contract-call? locked-token transfer amount sender LOCK-ADDRESS none))

      ;; update records
      (map-set user-lock-data {address: sender} user-lock-info-updated)
      (var-set total-amount (+ total-locked amount))
    )
    (ok true)
  )
)

;; split and share lock
(define-public (split-lock (addresses-info (list 200 {address: principal, amount: uint, withdrawal-address: principal}))) 
  (begin
    (try! (check-is-initialized))
    (try! (check-is-owner))
    (asserts! (is-eq (len (filter check-lock-amount-iter addresses-info)) u0) ERR-INVALID-AMOUNT)
    (let
      (
        (sender tx-sender)
        (total-amt (fold sum-lock-amount-iter addresses-info u0))
        (vested (var-get is-vested))
        (total-addresses- (var-get total-addresses))
        (user-lock-info (try! (get-user-lock-info sender)))
        (locked-amt (get total-amount user-lock-info))
        (total-claimed (get total-claimed user-lock-info))
        (user-lock-info-updated (merge user-lock-info {
          total-amount: (if (>= total-amt locked-amt) u0 (- locked-amt total-amt)),
        }))
      )

      (asserts! (not vested) ERR-ONLY-SINGLE-LOCK)
      (asserts! (< total-amt locked-amt) ERR-INVALID-AMOUNT)
      (asserts! (is-eq total-claimed u0) ERR-ALREADY-CLAIMED)
      (map-set user-lock-data {address: sender} user-lock-info-updated)
      (var-set total-addresses (+ total-addresses- (len addresses-info)))

      (fold store-lock-info-iter addresses-info u0)
    )
    (ok true)
  )
)

;; transferlockownership
(define-public (transfer-lock-ownership (new-owner principal) (withdrawal-address principal)) 
  (begin
    (try! (check-is-initialized))
    (try! (check-is-owner))
    (if (not (var-get is-vested))
      (let
        (
          (user-lock-info (try! (get-user-lock-info tx-sender)))
          (user-lock-amount (get total-amount user-lock-info))
          (total-claimed (get total-claimed user-lock-info))
          (claim-index (get claim-index user-lock-info))
          (last-claim-block (get last-claim-block user-lock-info))
        )
        (map-set user-lock-data
          {address: new-owner} 
          {
            last-claim-block: last-claim-block, 
            claim-index: claim-index, 
            total-claimed: total-claimed, 
            total-amount: user-lock-amount, 
            withdrawal-address: withdrawal-address
          }
        )
        (map-delete user-lock-data {address: tx-sender})
      )
      true
    )

    (ok (var-set lock-owner new-owner))
  )
)

;; PRIVATE CALLS

(define-private (check-is-owner)
  (ok (asserts! (is-eq contract-caller (var-get lock-owner)) ERR-NOT-AUTHORIZED))
)

(define-private (check-block-info-iter (blocks {height: uint, percentage: uint}))
  (not (and (> (get height blocks) block-height) (> (get percentage blocks) u0) (<= (get percentage blocks) u100)))
)

(define-private (check-block-heights-iter (blocks {height: uint, percentage: uint}) (data {check: bool, last-height: uint}))
  (let 
    (
      (height (get height blocks))
    )
    {check: (> height (get last-height data)), last-height: height}
  )
)

(define-private (check-lock-amount-iter (user-record {address: principal, amount: uint, withdrawal-address: principal}))
  (not (> (get amount user-record) u0))
)

(define-private (sum-lock-amount-iter (user-record {address: principal, amount: uint, withdrawal-address: principal}) (amount uint))
  (begin 
    (+ amount (get amount user-record))
  )
)

(define-private (sum-block-percentage-iter (blocks {height: uint, percentage: uint}) (total-percentage uint))
  (begin 
    (+ total-percentage (get percentage blocks))
  )
)

(define-private (store-lock-info-iter (user-record {address: principal, amount: uint, withdrawal-address: principal}) (count uint))
  (begin
    (map-set user-lock-data
      {address: (get address user-record)} 
      {
        last-claim-block: u0, 
        claim-index: u0, 
        total-claimed: u0, 
        total-amount: (get amount user-record), 
        withdrawal-address: (get withdrawal-address user-record)
      }
    )
    (+ count u1)
  )
)

;; PRIVATE CALLS
(define-private (check-is-initialized)
  (ok (asserts! (var-get initialized) ERR-NOT-INITIALIZED))
)


(define-public (initialize) 
  (begin
    (let
      (
        (token-amount u85621792120523)
        (fee-in-stx true)
        (vested false)
        (unlock-blocks-data (list {height: u172346, percentage: u100}))
        (addresses-info (list {address: 'SP3ATFW5VSD0W4N0E3K1E4CGFE8MJXQ9XFFMQ0HBY, amount: u85621792120523, withdrawal-address: 'SP3ATFW5VSD0W4N0E3K1E4CGFE8MJXQ9XFFMQ0HBY}))
      ) 
      (asserts! (is-eq (len (filter check-block-info-iter unlock-blocks-data)) u0) ERR-INVALID-BLOCK)
      (asserts! (is-eq (len (filter check-lock-amount-iter addresses-info)) u0) ERR-INVALID-AMOUNT)
      (asserts! (is-eq (fold sum-lock-amount-iter addresses-info u0) token-amount) ERR-INSUFFICIENT_AMOUNT)
      (asserts! (is-eq (fold sum-block-percentage-iter unlock-blocks-data u0) u100) ERR-INVALID-PERCENTAGE)
      (asserts! (get check (fold check-block-heights-iter unlock-blocks-data {check: false, last-height: block-height})) ERR-INVALID-BLOCK-ARR)

      (if fee-in-stx
        ;; Pay fee in STX
        (try! (stx-transfer? STX-FEE tx-sender 'SP2F4QC563WN0A0949WPH5W1YXVC4M1R46QKE0G14.memegoat-treasury)) 
        ;; Burn token
        (begin
          (try! (contract-call? 'SP2F4QC563WN0A0949WPH5W1YXVC4M1R46QKE0G14.memegoatstx burn SEC-TOKEN-FEE tx-sender))
        )
      )

      (if vested
        (begin
          (asserts! (> (len unlock-blocks-data) u1) ERR-INVALID-CONFIG)
        )
        (asserts! (and (is-eq (len unlock-blocks-data) u1) (is-eq (len addresses-info) u1)) ERR-INVALID-CONFIG)
      )

      (fold store-lock-info-iter addresses-info u0)
      
      ;; transfer token to LOCK ADDRESS
      (try! (contract-call? 'SP3ATFW5VSD0W4N0E3K1E4CGFE8MJXQ9XFFMQ0HBY.catdog-stxcity transfer token-amount tx-sender LOCK-ADDRESS none))

      (var-set lock-block block-height)
      (var-set total-amount token-amount)
      (var-set is-vested vested)
      (var-set unlock-blocks unlock-blocks-data)
      (var-set total-addresses (len addresses-info))
      
      (var-set initialized true)
    )
  (ok true)
  )
)

(begin 
  (initialize)
)  
  

Functions (17)

FunctionAccessArgs
get-token-lock-dataread-only
get-user-lock-inforead-onlyaddress: principal
is-dao-or-extensionread-only
emergency-withdrawpublictoken-trait: <ft-trait>, recipient: principal
withdraw-tokenpubliclocked-token: <ft-trait>
increment-lockpubliclocked-token: <ft-trait>, amount: uint
split-lockpublicaddresses-info: (list 200 {address: principal, amount: uint, withdrawal-address: principal}
transfer-lock-ownershippublicnew-owner: principal, withdrawal-address: principal
check-is-ownerprivate
check-block-info-iterprivateblocks: {height: uint, percentage: uint}
check-block-heights-iterprivateblocks: {height: uint, percentage: uint}, data: {check: bool, last-height: uint}
check-lock-amount-iterprivateuser-record: {address: principal, amount: uint, withdrawal-address: principal}
sum-lock-amount-iterprivateuser-record: {address: principal, amount: uint, withdrawal-address: principal}, amount: uint
sum-block-percentage-iterprivateblocks: {height: uint, percentage: uint}, total-percentage: uint
store-lock-info-iterprivateuser-record: {address: principal, amount: uint, withdrawal-address: principal}, count: uint
check-is-initializedprivate
initializepublic