Source Code

;; -------------------------------------------------------
;; TimeFi Vault Contract v-A2
;; Custodian pattern - DEPLOYER holds STX
;; Fixed for Clarity 4
;; -------------------------------------------------------

(define-constant ERR_UNAUTHORIZED (err u100))
(define-constant ERR_NOT_FOUND (err u101))
(define-constant ERR_INACTIVE (err u102))
(define-constant ERR_AMOUNT (err u103))
(define-constant ERR_LOCK_PERIOD (err u104))
(define-constant ERR_ALREADY (err u105))
(define-constant ERR_BOT (err u106))
(define-constant ERR_NO_BENEFICIARY (err u107))
(define-constant ERR_SAME_OWNER (err u108))
(define-constant ERR_PAUSED (err u109))

(define-constant MIN_DEPOSIT u10000)
;; Lock periods in blocks (1 block ~ 10 minutes)
(define-constant MIN_LOCK u6)          ;; ~1 hour (6 blocks)
(define-constant MAX_LOCK u52560)      ;; ~1 year (52560 blocks)
(define-constant FEE_BPS u50)
(define-constant BENEFICIARY_CLAIM_DELAY u12960) ;; 90 days (~12960 blocks)

(define-constant DEPLOYER tx-sender)

(define-data-var treasury principal tx-sender)
(define-data-var vault-nonce uint u0)
(define-data-var tvl uint u0)
(define-data-var fees uint u0)
(define-data-var protocol-paused bool false)

(define-map vaults
  uint
  {
    owner: principal,
    amount: uint,
    lock-time: uint,
    unlock-time: uint,
    active: bool,
    beneficiary: (optional principal)
  })

(define-map approved-bots-by-principal
  principal
  bool)

(define-map pending-transfers
  uint
  principal)

;; -------------------------------------------------------
;; HELPER: check if sender is an approved bot
;; -------------------------------------------------------

(define-read-only (is-bot (sender principal))
  (default-to false (map-get? approved-bots-by-principal sender)))

;; -------------------------------------------------------
;; PUBLIC: APPROVE BOT (admin only)
;; -------------------------------------------------------

(define-public (approve-bot (bot principal))
  (begin
    (asserts! (is-eq tx-sender DEPLOYER) ERR_UNAUTHORIZED)
    (map-set approved-bots-by-principal bot true)
    (print { event: "bot-approved", bot: bot })
    (ok true)))

;; -------------------------------------------------------
;; PUBLIC: REVOKE BOT APPROVAL (admin only)
;; -------------------------------------------------------

(define-public (revoke-bot (bot principal))
  (begin
    (asserts! (is-eq tx-sender DEPLOYER) ERR_UNAUTHORIZED)
    (map-set approved-bots-by-principal bot false)
    (print { event: "bot-revoked", bot: bot })
    (ok true)))

;; -------------------------------------------------------
;; PUBLIC: CREATE VAULT
;; User deposits STX to DEPLOYER (custodian pattern)
;; -------------------------------------------------------

(define-public (create-vault (amount uint) (lock-blocks uint))
  (let (
    (id (+ (var-get vault-nonce) u1))
    (fee (/ (* amount FEE_BPS) u10000))
    (deposit (- amount fee))
    (unlock (+ lock-blocks tenure-height))
  )
    (asserts! (not (var-get protocol-paused)) ERR_PAUSED)
    (asserts! (>= amount MIN_DEPOSIT) ERR_AMOUNT)
    (asserts! (>= lock-blocks MIN_LOCK) ERR_LOCK_PERIOD)
    (asserts! (<= lock-blocks MAX_LOCK) ERR_LOCK_PERIOD)

    ;; Transfer deposit to DEPLOYER (custodian)
    (try! (stx-transfer? deposit tx-sender DEPLOYER))
    ;; Transfer fee to treasury
    (try! (stx-transfer? fee tx-sender (var-get treasury)))

    ;; save vault
    (map-set vaults id
      {
        owner: tx-sender,
        amount: deposit,
        lock-time: tenure-height,
        unlock-time: unlock,
        active: true,
        beneficiary: none
      })

    (var-set vault-nonce id)
    (var-set tvl (+ (var-get tvl) deposit))
    (var-set fees (+ (var-get fees) fee))

    (print { event: "create", id: id, owner: tx-sender, amount: deposit, unlock: unlock })
    (ok id)))

;; -------------------------------------------------------
;; READ: GET VAULT
;; -------------------------------------------------------

(define-read-only (get-vault (id uint))
  (match (map-get? vaults id)
    v (ok v)
    ERR_NOT_FOUND))

;; -------------------------------------------------------
;; PUBLIC: PROCESS WITHDRAW (deployer sends STX to owner)
;; -------------------------------------------------------

(define-public (process-withdraw (id uint))
  (let (
    (vault (unwrap! (map-get? vaults id) ERR_NOT_FOUND))
    (vault-amount (get amount vault))
    (owner (get owner vault))
  )
    (asserts! (is-eq tx-sender DEPLOYER) ERR_UNAUTHORIZED)
    (asserts! (get active vault) ERR_INACTIVE)
    (asserts! (>= tenure-height (get unlock-time vault)) ERR_LOCK_PERIOD)

    ;; mark inactive
    (map-set vaults id
      (merge vault { amount: u0, active: false }))

    ;; DEPLOYER sends funds to owner
    (try! (stx-transfer? vault-amount tx-sender owner))

    (var-set tvl (- (var-get tvl) vault-amount))

    (print { event: "withdraw", id: id, owner: owner, amount: vault-amount })
    (ok true)))

;; -------------------------------------------------------
;; PUBLIC: REQUEST WITHDRAW (user initiates, deployer processes)
;; -------------------------------------------------------

(define-public (request-withdraw (id uint))
  (let (
    (vault (unwrap! (map-get? vaults id) ERR_NOT_FOUND))
  )
    (asserts! (not (var-get protocol-paused)) ERR_PAUSED)
    (asserts! (is-eq (get owner vault) tx-sender) ERR_UNAUTHORIZED)
    (asserts! (get active vault) ERR_INACTIVE)
    (asserts! (>= tenure-height (get unlock-time vault)) ERR_LOCK_PERIOD)

    (print { event: "withdraw-requested", id: id, owner: tx-sender, amount: (get amount vault) })
    (ok true)))

;; -------------------------------------------------------
;; READ: IS-ACTIVE
;; -------------------------------------------------------

(define-read-only (is-active (id uint))
  (match (map-get? vaults id)
    v (ok (get active v))
    ERR_NOT_FOUND))

;; -------------------------------------------------------
;; READ: GET TVL (Total Value Locked)
;; -------------------------------------------------------

(define-read-only (get-tvl)
  (ok (var-get tvl)))

;; -------------------------------------------------------
;; READ: GET TOTAL FEES COLLECTED
;; -------------------------------------------------------

(define-read-only (get-total-fees)
  (ok (var-get fees)))

;; -------------------------------------------------------
;; READ: GET VAULT COUNT
;; -------------------------------------------------------

(define-read-only (get-vault-count)
  (ok (var-get vault-nonce)))

;; -------------------------------------------------------
;; READ: GET TIME REMAINING UNTIL UNLOCK
;; -------------------------------------------------------

(define-read-only (get-time-remaining (id uint))
  (match (map-get? vaults id)
    vault
      (let (
        (now tenure-height)
        (unlock (get unlock-time vault))
      )
        (if (>= now unlock)
          (ok u0)
          (ok (- unlock now))))
    ERR_NOT_FOUND))

;; -------------------------------------------------------
;; READ: GET TREASURY ADDRESS
;; -------------------------------------------------------

(define-read-only (get-treasury)
  (ok (var-get treasury)))

;; -------------------------------------------------------
;; READ: CHECK IF VAULT CAN BE WITHDRAWN
;; -------------------------------------------------------

(define-read-only (can-withdraw (id uint))
  (match (map-get? vaults id)
    vault
      (ok (and (get active vault) (>= tenure-height (get unlock-time vault))))
    ERR_NOT_FOUND))

;; -------------------------------------------------------
;; READ: CHECK IF VAULT BELONGS TO OWNER
;; -------------------------------------------------------

(define-read-only (is-vault-owner (id uint) (owner principal))
  (match (map-get? vaults id)
    vault (ok (is-eq (get owner vault) owner))
    ERR_NOT_FOUND))

;; -------------------------------------------------------
;; READ: GET PROTOCOL CONSTANTS
;; -------------------------------------------------------

(define-read-only (get-min-deposit)
  MIN_DEPOSIT)

(define-read-only (get-min-lock)
  MIN_LOCK)

(define-read-only (get-max-lock)
  MAX_LOCK)

(define-read-only (get-fee-bps)
  FEE_BPS)

;; -------------------------------------------------------
;; READ: CALCULATE FEE FOR AMOUNT
;; -------------------------------------------------------

(define-read-only (calculate-fee (amount uint))
  (ok (/ (* amount FEE_BPS) u10000)))

(define-read-only (calculate-deposit-after-fee (amount uint))
  (let ((fee (/ (* amount FEE_BPS) u10000)))
    (ok (- amount fee))))

;; -------------------------------------------------------
;; PUBLIC: SET TREASURY (admin only)
;; -------------------------------------------------------

(define-public (set-treasury (new-treasury principal))
  (begin
    (asserts! (is-eq tx-sender DEPLOYER) ERR_UNAUTHORIZED)
    (var-set treasury new-treasury)
    (print { event: "treasury-updated", new: new-treasury })
    (ok true)))

;; -------------------------------------------------------
;; PUBLIC: TOP-UP VAULT (add more STX to existing vault)
;; -------------------------------------------------------

(define-public (top-up-vault (id uint) (amount uint))
  (let (
    (vault (unwrap! (map-get? vaults id) ERR_NOT_FOUND))
    (fee (/ (* amount FEE_BPS) u10000))
    (deposit (- amount fee))
  )
    (asserts! (not (var-get protocol-paused)) ERR_PAUSED)
    (asserts! (is-eq (get owner vault) tx-sender) ERR_UNAUTHORIZED)
    (asserts! (get active vault) ERR_INACTIVE)
    (asserts! (>= amount MIN_DEPOSIT) ERR_AMOUNT)

    ;; Transfer to DEPLOYER
    (try! (stx-transfer? deposit tx-sender DEPLOYER))
    (try! (stx-transfer? fee tx-sender (var-get treasury)))

    ;; update vault
    (map-set vaults id
      (merge vault { amount: (+ (get amount vault) deposit) }))

    (var-set tvl (+ (var-get tvl) deposit))
    (var-set fees (+ (var-get fees) fee))

    (print { event: "top-up", id: id, added: deposit, new-total: (+ (get amount vault) deposit) })
    (ok true)))

;; -------------------------------------------------------
;; PUBLIC: EXTEND LOCK DURATION
;; -------------------------------------------------------

(define-public (extend-lock (id uint) (additional-blocks uint))
  (let (
    (vault (unwrap! (map-get? vaults id) ERR_NOT_FOUND))
    (new-unlock (+ (get unlock-time vault) additional-blocks))
    (total-lock (- new-unlock (get lock-time vault)))
  )
    (asserts! (not (var-get protocol-paused)) ERR_PAUSED)
    (asserts! (is-eq (get owner vault) tx-sender) ERR_UNAUTHORIZED)
    (asserts! (get active vault) ERR_INACTIVE)
    (asserts! (> additional-blocks u0) ERR_LOCK_PERIOD)
    (asserts! (<= total-lock MAX_LOCK) ERR_LOCK_PERIOD)

    ;; update vault
    (map-set vaults id
      (merge vault { unlock-time: new-unlock }))

    (print { event: "extend-lock", id: id, new-unlock: new-unlock })
    (ok true)))

;; -------------------------------------------------------
;; PUBLIC: SET BENEFICIARY (heir/delegate for vault)
;; -------------------------------------------------------

(define-public (set-beneficiary (id uint) (beneficiary principal))
  (let (
    (vault (unwrap! (map-get? vaults id) ERR_NOT_FOUND))
  )
    (asserts! (is-eq (get owner vault) tx-sender) ERR_UNAUTHORIZED)
    (asserts! (get active vault) ERR_INACTIVE)
    (asserts! (not (is-eq beneficiary tx-sender)) ERR_SAME_OWNER)

    ;; update vault
    (map-set vaults id
      (merge vault { beneficiary: (some beneficiary) }))

    (print { event: "beneficiary-set", id: id, beneficiary: beneficiary })
    (ok true)))

;; -------------------------------------------------------
;; PUBLIC: REMOVE BENEFICIARY
;; -------------------------------------------------------

(define-public (remove-beneficiary (id uint))
  (let (
    (vault (unwrap! (map-get? vaults id) ERR_NOT_FOUND))
  )
    (asserts! (is-eq (get owner vault) tx-sender) ERR_UNAUTHORIZED)
    (asserts! (get active vault) ERR_INACTIVE)

    ;; update vault
    (map-set vaults id
      (merge vault { beneficiary: none }))

    (print { event: "beneficiary-removed", id: id })
    (ok true)))

;; -------------------------------------------------------
;; PUBLIC: PROCESS BENEFICIARY CLAIM (deployer processes)
;; -------------------------------------------------------

(define-public (process-beneficiary-claim (id uint))
  (let (
    (vault (unwrap! (map-get? vaults id) ERR_NOT_FOUND))
    (beneficiary-opt (get beneficiary vault))
    (vault-amount (get amount vault))
    (beneficiary (unwrap! beneficiary-opt ERR_NO_BENEFICIARY))
  )
    (asserts! (is-eq tx-sender DEPLOYER) ERR_UNAUTHORIZED)
    (asserts! (get active vault) ERR_INACTIVE)
    ;; Can only claim 90 days after unlock time
    (asserts! (>= tenure-height (+ (get unlock-time vault) BENEFICIARY_CLAIM_DELAY)) ERR_LOCK_PERIOD)

    ;; mark inactive
    (map-set vaults id
      (merge vault { amount: u0, active: false, beneficiary: none }))

    ;; DEPLOYER sends funds to beneficiary
    (try! (stx-transfer? vault-amount tx-sender beneficiary))

    (var-set tvl (- (var-get tvl) vault-amount))

    (print { event: "beneficiary-claim", id: id, beneficiary: beneficiary, amount: vault-amount })
    (ok true)))

;; -------------------------------------------------------
;; PUBLIC: REQUEST BENEFICIARY CLAIM (beneficiary initiates)
;; -------------------------------------------------------

(define-public (request-beneficiary-claim (id uint))
  (let (
    (vault (unwrap! (map-get? vaults id) ERR_NOT_FOUND))
    (beneficiary-opt (get beneficiary vault))
  )
    (asserts! (not (var-get protocol-paused)) ERR_PAUSED)
    (asserts! (get active vault) ERR_INACTIVE)
    (asserts! (is-some beneficiary-opt) ERR_NO_BENEFICIARY)
    (asserts! (is-eq tx-sender (unwrap-panic beneficiary-opt)) ERR_UNAUTHORIZED)
    (asserts! (>= tenure-height (+ (get unlock-time vault) BENEFICIARY_CLAIM_DELAY)) ERR_LOCK_PERIOD)

    (print { event: "beneficiary-claim-requested", id: id, beneficiary: tx-sender, amount: (get amount vault) })
    (ok true)))

;; -------------------------------------------------------
;; PUBLIC: INITIATE VAULT TRANSFER
;; -------------------------------------------------------

(define-public (initiate-transfer (id uint) (new-owner principal))
  (let (
    (vault (unwrap! (map-get? vaults id) ERR_NOT_FOUND))
  )
    (asserts! (is-eq (get owner vault) tx-sender) ERR_UNAUTHORIZED)
    (asserts! (get active vault) ERR_INACTIVE)
    (asserts! (not (is-eq new-owner tx-sender)) ERR_SAME_OWNER)

    (map-set pending-transfers id new-owner)

    (print { event: "transfer-initiated", id: id, from: tx-sender, to: new-owner })
    (ok true)))

;; -------------------------------------------------------
;; PUBLIC: ACCEPT VAULT TRANSFER
;; -------------------------------------------------------

(define-public (accept-transfer (id uint))
  (let (
    (vault (unwrap! (map-get? vaults id) ERR_NOT_FOUND))
    (new-owner (unwrap! (map-get? pending-transfers id) ERR_NOT_FOUND))
  )
    (asserts! (is-eq tx-sender new-owner) ERR_UNAUTHORIZED)
    (asserts! (get active vault) ERR_INACTIVE)

    ;; update vault owner
    (map-set vaults id
      (merge vault { owner: tx-sender }))

    ;; clear pending transfer
    (map-delete pending-transfers id)

    (print { event: "transfer-accepted", id: id, new-owner: tx-sender })
    (ok true)))

;; -------------------------------------------------------
;; PUBLIC: CANCEL VAULT TRANSFER
;; -------------------------------------------------------

(define-public (cancel-transfer (id uint))
  (let (
    (vault (unwrap! (map-get? vaults id) ERR_NOT_FOUND))
  )
    (asserts! (is-eq (get owner vault) tx-sender) ERR_UNAUTHORIZED)
    (map-delete pending-transfers id)
    (print { event: "transfer-cancelled", id: id })
    (ok true)))

;; -------------------------------------------------------
;; READ: GET PENDING TRANSFER
;; -------------------------------------------------------

(define-read-only (get-pending-transfer (id uint))
  (map-get? pending-transfers id))

;; -------------------------------------------------------
;; READ: GET BENEFICIARY
;; -------------------------------------------------------

(define-read-only (get-beneficiary (id uint))
  (match (map-get? vaults id)
    vault (ok (get beneficiary vault))
    ERR_NOT_FOUND))

;; -------------------------------------------------------
;; READ: IS PROTOCOL PAUSED
;; -------------------------------------------------------

(define-read-only (is-paused)
  (var-get protocol-paused))

;; -------------------------------------------------------
;; PUBLIC: PAUSE PROTOCOL (admin only)
;; -------------------------------------------------------

(define-public (pause-protocol)
  (begin
    (asserts! (is-eq tx-sender DEPLOYER) ERR_UNAUTHORIZED)
    (var-set protocol-paused true)
    (print { event: "protocol-paused" })
    (ok true)))

;; -------------------------------------------------------
;; PUBLIC: UNPAUSE PROTOCOL (admin only)
;; -------------------------------------------------------

(define-public (unpause-protocol)
  (begin
    (asserts! (is-eq tx-sender DEPLOYER) ERR_UNAUTHORIZED)
    (var-set protocol-paused false)
    (print { event: "protocol-unpaused" })
    (ok true)))

Functions (36)

FunctionAccessArgs
is-botread-onlysender: principal
approve-botpublicbot: principal
revoke-botpublicbot: principal
create-vaultpublicamount: uint, lock-blocks: uint
get-vaultread-onlyid: uint
process-withdrawpublicid: uint
request-withdrawpublicid: uint
is-activeread-onlyid: uint
get-tvlread-only
get-total-feesread-only
get-vault-countread-only
get-time-remainingread-onlyid: uint
get-treasuryread-only
can-withdrawread-onlyid: uint
is-vault-ownerread-onlyid: uint, owner: principal
get-min-depositread-only
get-min-lockread-only
get-max-lockread-only
get-fee-bpsread-only
calculate-feeread-onlyamount: uint
calculate-deposit-after-feeread-onlyamount: uint
set-treasurypublicnew-treasury: principal
top-up-vaultpublicid: uint, amount: uint
extend-lockpublicid: uint, additional-blocks: uint
set-beneficiarypublicid: uint, beneficiary: principal
remove-beneficiarypublicid: uint
process-beneficiary-claimpublicid: uint
request-beneficiary-claimpublicid: uint
initiate-transferpublicid: uint, new-owner: principal
accept-transferpublicid: uint
cancel-transferpublicid: uint
get-pending-transferread-onlyid: uint
get-beneficiaryread-onlyid: uint
is-pausedread-only
pause-protocolpublic
unpause-protocolpublic