๐Ÿ“ŽDeposits and Withdrawals

A look at how deposits and withdrawals of the STX token in the LST protocol work

Deposits

A user can join the protocol and deposit their STX anytime. This does not mean their STX tokens are earning yield immediately. A STX deposit only becomes active once the subsequent PoX stacking cycle starts. This can take up to two weeks (2100 Bitcoin blocks).

Furthermore, the protocol should calculate, behind the scenes, how much total inflow there is in a given cycle against the total outflow. Based on this, more STX tokens will be stacked (net inflow), no extra tokens will be stacked (inflow equals outflow) or if STX tokens need to be taken out of PoX and withdrawn by users (net outflow).

To handle smooth deposits, there will be a deposit โ€œcut-offโ€ for when the last STX tokens would be accepted for the next PoX cycle (the end period). As an example, this could be the last 60 Bitcoin blocks before the new stacking cycle, where this is enforceable on-chain. Either way, when depositing STX, the user receives stSTX at a shown UI ratio (which is calculated & enforced in Clarity).

Some sample code to do the above can be seen below:

(define-map cycle-info
  { 
    cycle-id: uint 
  }
  {
    deposited: uint,        ;; STX
    withdraw-init: uint,    ;; STX
    withdraw-out: uint,     ;; STX
    rewards: uint,          ;; STX
    commission: uint        ;; STX
  }
)

(define-public (deposit (reserve-trait <sticky-reserve-trait>) (stx-amount uint))
  (let (
    (cycle-id (get-pox-cycle))
    (current-cycle-info (get-cycle-info cycle-id))

    (stx-ststx (unwrap-panic (get-stx-per-ststx reserve-trait)))
    (ststx-to-receive (/ (* stx-amount u1000000) stx-ststx))
  )
    (try! (contract-call? .sticky-dao check-is-enabled))
    (try! (contract-call? .sticky-dao check-is-protocol (contract-of reserve-trait)))
    (asserts! (not (get-shutdown-deposits)) (err ERR_SHUTDOWN))

    (map-set cycle-info { cycle-id: cycle-id } (merge current-cycle-info { deposited: (+ (get deposited current-cycle-info) stx-amount) }))

    (try! (stx-transfer? stx-amount tx-sender (contract-of reserve-trait)))
    (try! (contract-call? .ststx-token mint-for-sticky ststx-to-receive tx-sender))

    (ok ststx-to-receive)
  )
)

In the above logic, the deposit function starts with requesting the next cycle ID from PoX (which is a positive number such as 65, 70 or whatever the current cycle ID is). Next, it requests the amount of deposits that have already happened so far in the cycle that is currently accepting deposits. The bulk of the logic in the function takes in STX from the tx-sender and keeps track of the deposits per cycle and the total amount of deposits. After this, an amount of stSTX will be minted based on the STX/stSTX ratio (discussed in another section).

Withdrawals

A user who wants to withdraw STX from the protocol (and burn their stSTX position) has two options:

  1. Instant Withdrawal

  2. End-of-Cycle Withdrawal

Instant Withdrawal

burn your stSTX to withdraw STX at a penalty (needs liquidity, but is profitable for market makers, they get the liquidity after PoX cycle ends and profitable)

End-of-Cycle Withdrawal

This way of withdrawing is the preferred method for any users when they do not want to incur a penalty. In this case, a user burns their stSTX position and receives an NFT that represents the position, which can be used to withdraw the STX at any time after the current PoX cycle ended.

Some sample code for the above logic could be as follows:

(define-map cycle-info
  { 
    cycle-id: uint 
  }
  {
    deposited: uint,        ;; STX
    withdraw-init: uint,    ;; STX
    withdraw-out: uint,     ;; STX
    rewards: uint,          ;; STX
    commission: uint        ;; STX
  }
)

(define-map withdrawals-by-address
  { 
    address: principal,
    cycle-id: uint 
  }
  {
    stx-amount: uint,
    ststx-amount: uint
  }
)

;; Initiate withdrawal, given stSTX amount and cycle
;; Can update amount as long as cycle not started
(define-public (init-withdraw (reserve-trait <sticky-reserve-trait>) (ststx-amount uint) (withdrawal-cycle uint))
  (let (
    (cycle-id (get-pox-cycle))
    (current-cycle-info (get-cycle-info withdrawal-cycle))

    (stx-ststx (unwrap-panic (get-stx-per-ststx reserve-trait)))
    (stx-to-receive (/ (* ststx-amount stx-ststx) u1000000))
    (stx-in-use (unwrap-panic (contract-call? reserve-trait get-stx-in-use)))

    (withdrawal-entry (get-withdrawals-by-address tx-sender withdrawal-cycle))
    (new-withdraw-init (+ (- (get withdraw-init current-cycle-info) (get stx-amount withdrawal-entry)) stx-to-receive))
  )
    (try! (contract-call? .sticky-dao check-is-enabled))
    (try! (contract-call? .sticky-dao check-is-protocol (contract-of reserve-trait)))
    (asserts! (not (get-shutdown-withdrawals)) (err ERR_SHUTDOWN))
    (asserts! (> withdrawal-cycle cycle-id) (err ERR_WRONG_CYCLE_ID))
    (asserts! (< new-withdraw-init (/ (* (get-withdrawal-treshold-per-cycle) stx-in-use) u10000)) (err ERR_WITHDRAW_EXCEEDED))

    ;; Update maps
    (map-set withdrawals-by-address { address: tx-sender, cycle-id: withdrawal-cycle } { stx-amount: stx-to-receive, ststx-amount: ststx-amount })
    (map-set cycle-info { cycle-id: withdrawal-cycle } (merge current-cycle-info { withdraw-init: new-withdraw-init }))

    ;; Transfer stSTX token to contract, only burn on actual withdraw
    (try! (contract-call? .ststx-token transfer ststx-amount tx-sender (as-contract tx-sender) none))

    (ok ststx-amount)
  )
)

;; Actual withdrawal for given cycle
(define-public (withdraw (reserve-trait <sticky-reserve-trait>) (withdrawal-cycle uint))
  (let (
    (cycle-id (get-pox-cycle))
    (current-cycle-info (get-cycle-info withdrawal-cycle))
    (withdrawal-entry (get-withdrawals-by-address tx-sender withdrawal-cycle))

    (receiver tx-sender)
    (stx-to-receive (get stx-amount withdrawal-entry))
  )
    (try! (contract-call? .sticky-dao check-is-enabled))
    (try! (contract-call? .sticky-dao check-is-protocol (contract-of reserve-trait)))
    (asserts! (not (get-shutdown-withdrawals)) (err ERR_SHUTDOWN))
    (asserts! (>= cycle-id withdrawal-cycle) (err ERR_WRONG_CYCLE_ID))

    ;; Update withdrawals maps so user can not withdraw again
    (map-set withdrawals-by-address { address: tx-sender, cycle-id: withdrawal-cycle } { stx-amount: u0, ststx-amount: u0 })
    (map-set cycle-info { cycle-id: withdrawal-cycle } (merge current-cycle-info { 
      withdraw-out: (+ (get withdraw-out current-cycle-info) stx-to-receive),
    }))

    ;; STX to user, burn stSTX
    (try! (as-contract (contract-call? reserve-trait request-stx stx-to-receive receiver)))
    (try! (contract-call? .ststx-token burn-for-sticky (get ststx-amount withdrawal-entry) (as-contract tx-sender)))

    (ok stx-to-receive)
  )
)

In the above implementation, no NFT is minted but the withdrawal request is kept in the contract using the withdrawals-by-adress method. Either method works, but the NFT method is preferred in production as it serves a double function. It helps the protocol to identify the position of the user, the position is still transferrable and it can be used as a reminder to the user that the protocol still owes STX to the user. The same init-withdraw method also burns the amount of stSTX sent into the method.

In the next section, we will look at how the STX per stSTX ratio is calculated.

Last updated