Source Code

;; SPDX-License-Identifier: BUSL-1.1

;; TOKENS
(define-fungible-token staked-lp-token)

;; ERRORS
(define-constant ERR-SENDER-MISMATCH (err u60000))
(define-constant ERR-LP-TOKEN-SUPPLY (err u60001))
(define-constant ERR-STAKED-LP-TOKEN-USER-NOT-ENOUGH-BALANCE (err u60002))
(define-constant ERR-MISSING-WITHDRAWAL (err u60003))
(define-constant ERR-WITHDRAWAL-NOT-FINALIZED (err u60004))
(define-constant ERR-ZERO-LP-TOKEN-STAKE (err u60005))
(define-constant ERR-ZERO-STAKED-LP-TOKEN-UNSTAKE (err u60006))
(define-constant ERR-NOT-GOVERNANCE (err u60007))
(define-constant ERR-INTEREST-PARAMS (err u60008))
(define-constant ERR-STAKING-DISABLED (err u60009))

;; Constants
(define-constant SUCCESS (ok true))
(define-constant SCALING-FACTOR u100000000)

;; lp-token
(define-constant token-prefix "gusdcx")

;; staking storages
(define-data-var withdrawal-finalization-period uint u100)
(define-data-var unfinalized-withdrawals {lp-tokens: uint, shares: uint} {lp-tokens: u0, shares: u0})
(define-map user-withdrawal-index principal uint)
(define-map user-withdrawals { user: principal, index: uint } { withdrawal-shares: uint, finalization-at: uint })
(define-data-var total-lp-tokens-staked uint u0)
(define-data-var staking-wiped-out bool false)

;; governance
(define-public (update-withdrawal-finalization-period (new-value uint))
  (begin
    (asserts! (is-eq contract-caller (contract-call? .state-v1 get-governance)) ERR-NOT-GOVERNANCE)
    (print {
      sender: contract-caller,
      old-withdrawal-finalization-period: (var-get withdrawal-finalization-period),
      new-withdrawal-finalization-period: new-value,
      action: "update-withdrawal-finalization-period"
    })
    (var-set withdrawal-finalization-period new-value)
    SUCCESS
  )
)

;; SIP-10 LP-TOKEN FUNCTIONS
(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-SENDER-MISMATCH)
    (asserts! (contract-call? .state-v1 is-staking-enabled) ERR-STAKING-DISABLED)
    (match memo to-print (print to-print) 0x)
    (try! (ft-transfer? staked-lp-token amount sender recipient))
    (print {
      sender: sender,
      recipient: recipient,
      amount: amount,
      memo: memo,
      action: "staked-lp-token-transfer"
    })
    SUCCESS
  )
)

(define-read-only (get-balance (account principal))
  (ok (ft-get-balance staked-lp-token account))
)

(define-read-only (get-total-supply)
  (ok (ft-get-supply staked-lp-token))
)

(define-read-only (get-name)
  (ok (concat token-prefix " - Granite Staked LP Token"))
)

(define-read-only (get-symbol)
  (ok (concat token-prefix "-GSLP"))
)

(define-read-only (get-decimals)
  ;; Lp token decimals -> market asset decimals
  (contract-call? .state-v1 get-decimals)
)

(define-read-only (get-token-uri)
  (ok none)
)

(define-read-only (get-next-user-withdrawal-index (user principal)) 
  (ok (map-get? user-withdrawal-index user))
)

;; Read only functions
;; lp-tokens * total-staked-lp-tokens / total-lp-token
(define-read-only (convert-to-staked-lp-tokens (lp-tokens uint) (round-up bool))
  (let (
      (total-staked-lp-tokens (ft-get-supply staked-lp-token))
      (total-lp-tokens (var-get total-lp-tokens-staked))
    )
    (if (is-eq total-staked-lp-tokens u0)
      lp-tokens
      (contract-call? .math-v1 divide round-up (* total-staked-lp-tokens lp-tokens) total-lp-tokens)
    )
))

;; staked-lp-tokens * total-lp-tokens / total-staked-lp-tokens
(define-read-only (convert-to-lp-tokens (staked-lp-tokens uint) (round-up bool))
  (let (
      (total-staked-lp-tokens (ft-get-supply staked-lp-token))
      (total-lp-tokens (var-get total-lp-tokens-staked))
    )
    (if (is-eq total-lp-tokens u0)
      staked-lp-tokens
      (contract-call? .math-v1 divide round-up (* staked-lp-tokens total-lp-tokens) total-staked-lp-tokens)
    )
))

;; lp-tokens * total-withdraw-share / total-lp-tokens-in-withdrawal
(define-read-only (convert-to-withdrawal-shares (lp-tokens uint))
  (let (
      (unfinalized-withdrawals-info (var-get unfinalized-withdrawals))
      (total-withdrawal-shares (get shares unfinalized-withdrawals-info))
      (total-withdrawal-lp-tokens (get lp-tokens unfinalized-withdrawals-info))
    )
    (if (is-eq total-withdrawal-shares u0)
      lp-tokens
      (contract-call? .math-v1 divide false (* total-withdrawal-shares lp-tokens) total-withdrawal-lp-tokens)
    )
))

;; withdrawal-shares * total-withdrawal-lp-tokens / total-withdrawal-shares
(define-read-only (convert-to-withdrawal-lp-tokens (withdrawal-shares uint))
  (let (
      (unfinalized-withdrawals-info (var-get unfinalized-withdrawals))
      (total-withdrawal-shares (get shares unfinalized-withdrawals-info))
      (total-withdrawal-lp-tokens (get lp-tokens unfinalized-withdrawals-info))
    )
    (if (is-eq total-withdrawal-lp-tokens u0)
      u0
      (contract-call? .math-v1 divide false (* withdrawal-shares total-withdrawal-lp-tokens) total-withdrawal-shares)
    )
))

(define-read-only (get-withdrawal (user principal) (index uint))
  (map-get? user-withdrawals {user: user, index: index})
)

(define-read-only (get-active-staked-lp-tokens)
  (var-get total-lp-tokens-staked)
)

(define-read-only (get-total-staked-lp-tokens)
  (+ (var-get total-lp-tokens-staked) (get lp-tokens (var-get unfinalized-withdrawals)))
)

(define-read-only (get-withdrawal-finalization-period)
  (var-get withdrawal-finalization-period)
)

(define-read-only (is-staking-wiped-out)
  (var-get staking-wiped-out)
)

;; Public functions
(define-public (stake (lp-tokens uint))
  (let (
      (staking-enabled (try! (check-staking-enabled)))
      (staked-lp-tokens-to-mint (convert-to-staked-lp-tokens lp-tokens false))
    )
    (try! (accrue-interest))
    (asserts! (> lp-tokens u0) ERR-ZERO-LP-TOKEN-STAKE)
    ;; transfer lp-tokens to staking contract and mint staked lp tokens to user
    (try! (contract-call? .state-v1 transfer lp-tokens contract-caller (as-contract contract-caller) none))
    (try! (ft-mint? staked-lp-token staked-lp-tokens-to-mint contract-caller))
    (var-set total-lp-tokens-staked (+ (var-get total-lp-tokens-staked) lp-tokens))
    (print {
      sender: contract-caller,
      lp-tokens: lp-tokens,
      staked-lp-tokens: staked-lp-tokens-to-mint,
      action: "lp-token-staked"
    })
    SUCCESS
))

(define-public (increase-lp-staked-balance (lp-tokens uint))
  (begin
    (try! (contract-call? .state-v1 is-allowed-contract contract-caller))
    (var-set total-lp-tokens-staked (+ (var-get total-lp-tokens-staked) lp-tokens))
    (print {
      action: "increase-lp-staked-balance",
      amount: lp-tokens,
    })
    SUCCESS
  )
)


(define-public (slash-total-staked-lp-tokens (lp-tokens uint))
  (let (
      (total-staked-lp-tokens (get-total-staked-lp-tokens))
      (unfinalized-withdrawal-info (var-get unfinalized-withdrawals))
      (withdrawal-lp-tokens (get lp-tokens unfinalized-withdrawal-info))
      (withdrawal-lp-token-rate (/ (* withdrawal-lp-tokens SCALING-FACTOR) total-staked-lp-tokens))
      (withdrawal-lp-tokens-to-slash (/ (* lp-tokens withdrawal-lp-token-rate) SCALING-FACTOR))
      (active-staked-lp-tokens-to-slash (- lp-tokens withdrawal-lp-tokens-to-slash))
    ) 
    (try! (contract-call? .state-v1 is-allowed-contract contract-caller))
    (var-set total-lp-tokens-staked (- (var-get total-lp-tokens-staked) active-staked-lp-tokens-to-slash))
    (if (is-eq withdrawal-lp-tokens withdrawal-lp-tokens-to-slash)
      (var-set unfinalized-withdrawals {
        lp-tokens: u0,
        shares: u0,
      })
      (var-set unfinalized-withdrawals {
        lp-tokens: (- withdrawal-lp-tokens withdrawal-lp-tokens-to-slash),
        shares: (get shares unfinalized-withdrawal-info),
      })
    )
    (if (is-eq lp-tokens total-staked-lp-tokens)
      (begin 
        (var-set staking-wiped-out true)
        (print {
          action: "staking-wipe-out",
          amount: lp-tokens,
        })
        SUCCESS
      )

      (begin 
        (print {
          action: "slash-total-staked-lp-tokens",
          amount: lp-tokens,
        })
        SUCCESS  
      )
)))

(define-public (reconcile-lp-token-balance)
  (let (
      (current-balance (unwrap! (contract-call? .state-v1 get-balance (as-contract contract-caller)) ERR-LP-TOKEN-SUPPLY))
      (staked-lp-tokens (var-get total-lp-tokens-staked))
      (accounted-lp-tokens (+ staked-lp-tokens (get lp-tokens (var-get unfinalized-withdrawals))))
    )
    (try! (check-staking-enabled))
    (try! (accrue-interest))
    (asserts! (is-eq (contract-call? .state-v1 get-governance) contract-caller) ERR-NOT-GOVERNANCE)
    (if (>= current-balance accounted-lp-tokens)
      (var-set total-lp-tokens-staked (+ staked-lp-tokens (- current-balance accounted-lp-tokens)))
      (var-set total-lp-tokens-staked (- staked-lp-tokens (- accounted-lp-tokens current-balance)))
    )
    (print {
      action: "reconcile-lp-token-balance",
      previous-staked-lp-tokens: staked-lp-tokens,
      new-staked-lp-tokens: (var-get total-lp-tokens-staked)
    })  
    SUCCESS
  )
)

(define-public (initiate-unstake (staked-lp-tokens uint))
  (let (
      (staking-enabled (try! (check-staking-enabled)))
      (user contract-caller)
      (user-total-balance (ft-get-balance staked-lp-token user))
      (withdrawal-index (default-to u0 (map-get? user-withdrawal-index user)))
      (finalization-at (+ stacks-block-height (var-get withdrawal-finalization-period)))
      (lp-tokens-to-return (convert-to-lp-tokens staked-lp-tokens false))
      (withdraw-shares (convert-to-withdrawal-shares lp-tokens-to-return))
      (unfinalized-withdrawal-info (var-get unfinalized-withdrawals))
    )
    (try! (accrue-interest))
    (asserts! (> staked-lp-tokens u0) ERR-ZERO-STAKED-LP-TOKEN-UNSTAKE)
    (asserts! (>= user-total-balance staked-lp-tokens) ERR-STAKED-LP-TOKEN-USER-NOT-ENOUGH-BALANCE)
    (map-set user-withdrawal-index user (+ withdrawal-index u1))
    (try! (ft-burn? staked-lp-token staked-lp-tokens user))
    (map-set user-withdrawals {user: user, index: withdrawal-index} {withdrawal-shares: withdraw-shares, finalization-at: finalization-at})
    (var-set total-lp-tokens-staked (- (var-get total-lp-tokens-staked) lp-tokens-to-return))
    (var-set unfinalized-withdrawals {
      lp-tokens: (+ (get lp-tokens unfinalized-withdrawal-info) lp-tokens-to-return),
      shares: (+ (get shares unfinalized-withdrawal-info) withdraw-shares),
    })
    (print {
      sender: user,
      index: withdrawal-index,
      amount: staked-lp-tokens,
      action: "initiated-unstake"
    })
    (ok withdrawal-index)
))

(define-public (finalize-unstake (index uint))
  (let (
      (staking-enabled (try! (check-staking-enabled)))
      (user contract-caller)
      (unfinalized-withdrawal-info (var-get unfinalized-withdrawals))
      (withdrawal (unwrap! (map-get? user-withdrawals {user: user, index: index}) ERR-MISSING-WITHDRAWAL))
      (withdraw-shares (get withdrawal-shares withdrawal))
      (lp-tokens-to-return (convert-to-withdrawal-lp-tokens withdraw-shares))
      (finalization-at (get finalization-at withdrawal))
    )
    (asserts! (>= stacks-block-height finalization-at) ERR-WITHDRAWAL-NOT-FINALIZED)
    (try! (if (> lp-tokens-to-return u0)
      (as-contract (contract-call? .state-v1 transfer lp-tokens-to-return (as-contract contract-caller) user none))
      SUCCESS
    ))
    (var-set unfinalized-withdrawals {
      lp-tokens: (- (get lp-tokens unfinalized-withdrawal-info) lp-tokens-to-return),
      shares: (- (get shares unfinalized-withdrawal-info) withdraw-shares),
    })
    (map-delete user-withdrawals {user: user, index: index})
     (print {
      sender: user,
      index: index,
      lp-tokens: lp-tokens-to-return,
      action: "finalize-unstake"
    })
    SUCCESS
  )
)

;; private functions
(define-private (accrue-interest)
  (let (
    (accrue-interest-params (unwrap! (contract-call? .state-v1 get-accrue-interest-params) ERR-INTEREST-PARAMS))
    (accrued-interest (try! (contract-call? .linear-kinked-ir-v1 accrue-interest
      (get last-accrued-block-time accrue-interest-params)
      (get lp-interest accrue-interest-params)
      (get staked-interest accrue-interest-params)
      (try! (contract-call? .staking-reward-v1 calculate-staking-reward-percentage (get-active-staked-lp-tokens)))
      (get protocol-interest accrue-interest-params)
      (get protocol-reserve-percentage accrue-interest-params)
      (get total-assets accrue-interest-params)))
    )
  )
  (contract-call? .state-v1 set-accrued-interest accrued-interest)
))


(define-private (check-staking-enabled)
  (let (
    (staking-disabled (not (contract-call? .state-v1 is-staking-enabled)))
    (wiped-out (var-get staking-wiped-out))
  )
    (if (or staking-disabled wiped-out)
      ERR-STAKING-DISABLED
      (ok true)
    )
  )
)

Functions (26)

FunctionAccessArgs
update-withdrawal-finalization-periodpublicnew-value: uint
transferpublicamount: uint, sender: principal, recipient: principal, memo: (optional (buff 34
get-balanceread-onlyaccount: principal
get-total-supplyread-only
get-nameread-only
get-symbolread-only
get-decimalsread-only
get-token-uriread-only
get-next-user-withdrawal-indexread-onlyuser: principal
convert-to-staked-lp-tokensread-onlylp-tokens: uint, round-up: bool
convert-to-lp-tokensread-onlystaked-lp-tokens: uint, round-up: bool
convert-to-withdrawal-sharesread-onlylp-tokens: uint
convert-to-withdrawal-lp-tokensread-onlywithdrawal-shares: uint
get-withdrawalread-onlyuser: principal, index: uint
get-active-staked-lp-tokensread-only
get-total-staked-lp-tokensread-only
get-withdrawal-finalization-periodread-only
is-staking-wiped-outread-only
stakepubliclp-tokens: uint
increase-lp-staked-balancepubliclp-tokens: uint
slash-total-staked-lp-tokenspubliclp-tokens: uint
reconcile-lp-token-balancepublic
initiate-unstakepublicstaked-lp-tokens: uint
finalize-unstakepublicindex: uint
accrue-interestprivate
check-staking-enabledprivate