Source Code

;; Unstaking Manager Contract
;; Handles unstaking requests, cooldown periods, and emergency withdrawals

;; Constants
(define-constant contract-owner tx-sender)
(define-constant err-owner-only (err u300))
(define-constant err-not-found (err u301))
(define-constant err-invalid-amount (err u302))
(define-constant err-already-exists (err u303))
(define-constant err-cooldown-active (err u304))
(define-constant err-cooldown-not-complete (err u305))
(define-constant err-no-stake (err u306))
(define-constant err-emergency-only (err u307))

;; Penalty and cooldown constants
(define-constant early-unstake-penalty u20) ;; 20% penalty
(define-constant cooldown-period u1440) ;; ~10 days in blocks
(define-constant emergency-penalty u30) ;; 30% penalty for emergency withdrawal

;; Data Variables
(define-data-var emergency-mode bool false)
(define-data-var total-penalties-collected uint u0)
(define-data-var penalty-recipient principal contract-owner)

;; Unstaking request structure
(define-map unstaking-requests
  principal
  {
    amount: uint,
    initiation-block: uint,
    is-early: bool,
    penalty-amount: uint,
    original-stake-start: uint,
    lock-period: uint
  }
)

;; Track completed unstakes for history
(define-map unstake-history
  { user: principal, unstake-id: uint }
  {
    amount: uint,
    penalty: uint,
    completed-block: uint,
    was-early: bool
  }
)

(define-data-var unstake-counter uint u0)

;; Emergency withdrawal tracking
(define-map emergency-withdrawals
  principal
  {
    amount: uint,
    penalty: uint,
    block: uint
  }
)

;; Read-only functions

(define-read-only (get-unstaking-request (user principal))
  (ok (map-get? unstaking-requests user))
)

(define-read-only (get-cooldown-period)
  (ok cooldown-period)
)

(define-read-only (get-early-penalty-rate)
  (ok early-unstake-penalty)
)

(define-read-only (get-emergency-penalty-rate)
  (ok emergency-penalty)
)

(define-read-only (is-emergency-mode)
  (ok (var-get emergency-mode))
)

(define-read-only (get-total-penalties-collected)
  (ok (var-get total-penalties-collected))
)

(define-read-only (get-penalty-recipient)
  (ok (var-get penalty-recipient))
)

(define-read-only (is-cooldown-complete (user principal))
  (match (map-get? unstaking-requests user)
    request
      (let
        (
          (completion-block (+ (get initiation-block request) cooldown-period))
        )
        (ok (>= stacks-block-height completion-block))
      )
    (ok false)
  )
)

(define-read-only (get-cooldown-remaining (user principal))
  (match (map-get? unstaking-requests user)
    request
      (let
        (
          (completion-block (+ (get initiation-block request) cooldown-period))
        )
        (ok (if (>= stacks-block-height completion-block)
          u0
          (- completion-block stacks-block-height)
        ))
      )
    err-not-found
  )
)

(define-read-only (calculate-penalty (amount uint) (is-early bool))
  (if is-early
    (ok (/ (* amount early-unstake-penalty) u100))
    (ok u0)
  )
)

(define-read-only (calculate-emergency-penalty (amount uint))
  (ok (/ (* amount emergency-penalty) u100))
)

(define-read-only (get-unstake-estimate (amount uint) (is-early bool))
  (let
    (
      (penalty (if is-early (/ (* amount early-unstake-penalty) u100) u0))
      (net-amount (- amount penalty))
    )
    (ok {
      gross-amount: amount,
      penalty: penalty,
      net-amount: net-amount,
      penalty-rate: (if is-early early-unstake-penalty u0)
    })
  )
)

;; Public functions

(define-public (initiate-unstake 
  (amount uint) 
  (stake-start-block uint) 
  (lock-period uint))
  (let
    (
      (existing-request (map-get? unstaking-requests tx-sender))
      (unlock-block (+ stake-start-block lock-period))
      (is-early (< stacks-block-height unlock-block))
      (penalty (if is-early (/ (* amount early-unstake-penalty) u100) u0))
    )
    (asserts! (is-none existing-request) err-already-exists)
    (asserts! (> amount u0) err-invalid-amount)
    
    (map-set unstaking-requests tx-sender {
      amount: amount,
      initiation-block: stacks-block-height,
      is-early: is-early,
      penalty-amount: penalty,
      original-stake-start: stake-start-block,
      lock-period: lock-period
    })
    
    (print {
      event: "unstake-initiated",
      user: tx-sender,
      amount: amount,
      is-early: is-early,
      penalty: penalty,
      cooldown-complete-at: (+ stacks-block-height cooldown-period),
      block: stacks-block-height
    })
    
    (ok {
      amount: amount,
      penalty: penalty,
      net-amount: (- amount penalty),
      cooldown-blocks: cooldown-period
    })
  )
)

(define-public (complete-unstake)
  (let
    (
      (request (unwrap! (map-get? unstaking-requests tx-sender) err-not-found))
      (completion-block (+ (get initiation-block request) cooldown-period))
      (amount (get amount request))
      (penalty (get penalty-amount request))
      (net-amount (- amount penalty))
    )
    (asserts! (>= stacks-block-height completion-block) err-cooldown-not-complete)
    
    ;; Record penalty if any
    (if (> penalty u0)
      (var-set total-penalties-collected (+ (var-get total-penalties-collected) penalty))
      true
    )
    
    ;; Note: In production, transfer tokens here
    ;; (try! (as-contract (stx-transfer? net-amount tx-sender tx-sender)))
    
    ;; Save to history
    (map-set unstake-history
      { user: tx-sender, unstake-id: (var-get unstake-counter) }
      {
        amount: amount,
        penalty: penalty,
        completed-block: stacks-block-height,
        was-early: (get is-early request)
      }
    )
    
    (var-set unstake-counter (+ (var-get unstake-counter) u1))
    (map-delete unstaking-requests tx-sender)
    
    (print {
      event: "unstake-completed",
      user: tx-sender,
      amount: amount,
      penalty: penalty,
      net-amount: net-amount,
      block: stacks-block-height
    })
    
    (ok net-amount)
  )
)

(define-public (cancel-unstake-request)
  (let
    (
      (request (unwrap! (map-get? unstaking-requests tx-sender) err-not-found))
    )
    (map-delete unstaking-requests tx-sender)
    
    (print {
      event: "unstake-cancelled",
      user: tx-sender,
      amount: (get amount request),
      block: stacks-block-height
    })
    
    (ok true)
  )
)

(define-public (emergency-withdraw (amount uint))
  (let
    (
      (penalty (/ (* amount emergency-penalty) u100))
      (net-amount (- amount penalty))
    )
    (asserts! (var-get emergency-mode) err-emergency-only)
    (asserts! (> amount u0) err-invalid-amount)
    
    ;; Record penalty
    (var-set total-penalties-collected (+ (var-get total-penalties-collected) penalty))
    
    ;; Note: In production, transfer tokens here
    ;; (try! (as-contract (stx-transfer? net-amount tx-sender tx-sender)))
    
    (map-set emergency-withdrawals tx-sender {
      amount: amount,
      penalty: penalty,
      block: stacks-block-height
    })
    
    (print {
      event: "emergency-withdrawal",
      user: tx-sender,
      amount: amount,
      penalty: penalty,
      net-amount: net-amount,
      block: stacks-block-height
    })
    
    (ok net-amount)
  )
)

;; Admin functions

(define-public (set-emergency-mode (enabled bool))
  (begin
    (asserts! (is-eq tx-sender contract-owner) err-owner-only)
    (var-set emergency-mode enabled)
    (print {
      event: "emergency-mode-updated",
      enabled: enabled,
      block: stacks-block-height
    })
    (ok true)
  )
)

(define-public (set-penalty-recipient (new-recipient principal))
  (begin
    (asserts! (is-eq tx-sender contract-owner) err-owner-only)
    (var-set penalty-recipient new-recipient)
    (ok true)
  )
)

(define-public (withdraw-penalties (amount uint))
  (begin
    (asserts! (is-eq tx-sender contract-owner) err-owner-only)
    (asserts! (>= (var-get total-penalties-collected) amount) err-invalid-amount)
    
    ;; Note: In production, transfer penalties to recipient
    ;; (try! (as-contract (stx-transfer? amount tx-sender (var-get penalty-recipient))))
    
    (var-set total-penalties-collected (- (var-get total-penalties-collected) amount))
    (ok true)
  )
)

;; Force complete an unstake (admin only, for emergencies)
(define-public (force-complete-unstake (user principal))
  (let
    (
      (request (unwrap! (map-get? unstaking-requests user) err-not-found))
      (amount (get amount request))
      (penalty (get penalty-amount request))
      (net-amount (- amount penalty))
    )
    (asserts! (is-eq tx-sender contract-owner) err-owner-only)
    
    (if (> penalty u0)
      (var-set total-penalties-collected (+ (var-get total-penalties-collected) penalty))
      true
    )
    
    (map-set unstake-history
      { user: user, unstake-id: (var-get unstake-counter) }
      {
        amount: amount,
        penalty: penalty,
        completed-block: stacks-block-height,
        was-early: (get is-early request)
      }
    )
    
    (var-set unstake-counter (+ (var-get unstake-counter) u1))
    (map-delete unstaking-requests user)
    
    (print {
      event: "unstake-force-completed",
      user: user,
      admin: tx-sender,
      amount: amount,
      penalty: penalty,
      block: stacks-block-height
    })
    
    (ok net-amount)
  )
)

;; Get user's unstake history
(define-read-only (get-unstake-history (user principal) (unstake-id uint))
  (ok (map-get? unstake-history { user: user, unstake-id: unstake-id }))
)

;; Get user's emergency withdrawal info
(define-read-only (get-emergency-withdrawal (user principal))
  (ok (map-get? emergency-withdrawals user))
)

Functions (22)

FunctionAccessArgs
get-unstaking-requestread-onlyuser: principal
get-cooldown-periodread-only
get-early-penalty-rateread-only
get-emergency-penalty-rateread-only
is-emergency-moderead-only
get-total-penalties-collectedread-only
get-penalty-recipientread-only
is-cooldown-completeread-onlyuser: principal
get-cooldown-remainingread-onlyuser: principal
calculate-penaltyread-onlyamount: uint, is-early: bool
calculate-emergency-penaltyread-onlyamount: uint
get-unstake-estimateread-onlyamount: uint, is-early: bool
initiate-unstakepublicamount: uint, stake-start-block: uint, lock-period: uint
complete-unstakepublic
cancel-unstake-requestpublic
emergency-withdrawpublicamount: uint
set-emergency-modepublicenabled: bool
set-penalty-recipientpublicnew-recipient: principal
withdraw-penaltiespublicamount: uint
force-complete-unstakepublicuser: principal
get-unstake-historyread-onlyuser: principal, unstake-id: uint
get-emergency-withdrawalread-onlyuser: principal