Source Code

;; title: PiggyBank
;; version: 1.0.0
;; summary: Individual savings account with lock duration and penalty fees
;; description: Allows users to deposit supported tokens and withdraw after lock period. Early withdrawals incur a penalty fee.

;; SIP-010 Fungible Token Trait
(define-trait sip010-token
    (
        ;; Transfer from the origin to a new principal
        (transfer (uint principal principal) (response bool uint))
        ;; Get the token name
        (get-name () (response (string-ascii 32) uint))
        ;; Get the token symbol
        (get-symbol () (response (string-ascii 32) uint))
        ;; Get the number of decimals used
        (get-decimals () (response uint uint))
        ;; Get the balance of a principal
        (get-balance (principal) (response uint uint))
        ;; Get the total supply
        (get-total-supply () (response uint uint))
        ;; Get the token URI
        (get-token-uri () (response (optional (string-ascii 256)) uint))
    )
)

(define-constant ERR-UNAUTHORIZED (err u1001))
(define-constant ERR-INVALID-AMOUNT (err u1002))
(define-constant ERR-LOCK-NOT-EXPIRED (err u1003))
(define-constant ERR-NO-BALANCE (err u1004))
(define-constant ERR-INVALID-TOKEN (err u1005))
(define-constant ERR-TRANSFER-FAILED (err u1006))

(define-constant PENALTY-RATE u50) ;; 5% penalty (50 basis points = 5%)

;; Data vars
(define-data-var contract-principal (optional principal) none)

;; Data maps
(define-map balances { token: principal, owner: principal } uint)
(define-map lock-info { owner: principal } { 
    lock-duration: uint,
    lock-start: uint,
    token: principal
})

;; Public functions

;; Deposit STX into the piggy bank
(define-public (deposit-stx (amount uint))
    (let ((sender tx-sender))
        (begin
            ;; Initialize contract principal if needed (use sender as marker for first call)
            (if (is-none (var-get contract-principal))
                (var-set contract-principal (some sender))
                true
            )
            
            ;; Verify amount is greater than 0
            (asserts! (> amount u0) ERR-INVALID-AMOUNT)
            
            ;; Get STX marker
            (let ((stx-marker (unwrap-panic (var-get contract-principal))))
                ;; Check if lock already exists
                (let ((existing-lock (map-get? lock-info { owner: sender })))
                    (if (is-some existing-lock)
                        ;; If lock exists, verify it's STX
                        (asserts! (is-eq (get token (unwrap-panic existing-lock)) stx-marker) ERR-INVALID-TOKEN)
                        ;; If no lock exists, create one
                        (let ((current-block u0)) ;; TODO: Replace with block-height when available in Clarity 4
                            (map-set lock-info { owner: sender } {
                                lock-duration: u0,
                                lock-start: current-block,
                                token: stx-marker
                            })
                        )
                    )
                )
                
                ;; Transfer STX from sender to this contract
                ;; Note: User must send STX as part of the transaction
                ;; The contract receives STX automatically when the function is called with STX
                (begin
                    ;; Update balance (use contract principal as token identifier for STX)
                    (let ((current-balance (default-to u0 (map-get? balances { token: stx-marker, owner: sender }))))
                        (map-set balances { token: stx-marker, owner: sender } (+ current-balance amount))
                    )
                    (ok true)
                )
            )
        )
    )
)

;; Deposit fungible tokens into the piggy bank
(define-public (deposit-token (amount uint) (token <sip010-token>))
    (let ((sender tx-sender)
          (token-principal (contract-of token)))
        (begin
            ;; Verify amount is greater than 0
            (asserts! (> amount u0) ERR-INVALID-AMOUNT)
            
            ;; Check if lock already exists
            (let ((existing-lock (map-get? lock-info { owner: sender })))
                (if (is-some existing-lock)
                    ;; If lock exists, verify it's the same token
                    (asserts! (is-eq (get token (unwrap-panic existing-lock)) token-principal) ERR-INVALID-TOKEN)
                    ;; If no lock exists, create one
                    (let ((current-block u0)) ;; TODO: Replace with block-height when available in Clarity 4
                        (map-set lock-info { owner: sender } {
                            lock-duration: u0,
                            lock-start: current-block,
                            token: token-principal
                        })
                    )
                )
            )
            
            ;; Transfer tokens from sender to this contract
            (match (contract-call? token transfer amount sender tx-sender)
                success
                    (begin
                        ;; Update balance
                        (let ((current-balance (default-to u0 (map-get? balances { token: token-principal, owner: sender }))))
                            (map-set balances { token: token-principal, owner: sender } (+ current-balance amount))
                        )
                        (ok true)
                    )
                error ERR-TRANSFER-FAILED
            )
        )
    )
)

;; Set lock duration (can only be set once, before first deposit)
(define-public (set-lock-duration (duration uint))
    (let ((lock (map-get? lock-info { owner: tx-sender })))
        (if (is-some lock)
            (let ((lock-data (unwrap-panic lock)))
                (let ((balance (default-to u0 (map-get? balances { 
                    token: (get token lock-data), 
                    owner: tx-sender 
                }))))
                    ;; Can only set lock duration if no balance exists
                    (asserts! (is-eq balance u0) ERR-UNAUTHORIZED)
                    (map-set lock-info { owner: tx-sender } {
                        lock-duration: duration,
                        lock-start: (get lock-start lock-data),
                        token: (get token lock-data)
                    })
                    (ok true)
                )
            )
            ERR-UNAUTHORIZED
        )
    )
)

;; Withdraw tokens from the piggy bank
;; Note: This function requires the contract to hold STX/tokens to transfer
;; The actual transfer will be handled via post-conditions or a separate claim function
(define-public (withdraw (amount uint))
    (let ((sender tx-sender)
          (lock (map-get? lock-info { owner: tx-sender })))
        (asserts! (is-some lock) ERR-UNAUTHORIZED)
        
        (let ((lock-data (unwrap-panic lock))
              (token (get token lock-data))
              (lock-start (get lock-start lock-data))
              (lock-duration (get lock-duration lock-data))
              (current-balance (default-to u0 (map-get? balances { token: token, owner: sender }))))
            
            ;; Verify amount is valid
            (asserts! (> amount u0) ERR-INVALID-AMOUNT)
            (asserts! (<= amount current-balance) ERR-NO-BALANCE)
            
            ;; Calculate if lock period has expired
            (let ((current-height stacks-block-height)
                  (elapsed (- current-height lock-start))
                  (is-locked (< elapsed lock-duration)))
                
                (let ((withdraw-amount (if is-locked
                        ;; Calculate penalty for early withdrawal
                        (let ((penalty-amount (/ (* amount PENALTY-RATE) u1000)))
                            (- amount penalty-amount)
                        )
                        ;; No penalty if lock period expired
                        amount
                    )))
                    
                    ;; Update balance
                    (map-set balances { token: token, owner: sender } (- current-balance amount))
                    
                    ;; Return the withdraw amount
                    ;; Note: Actual STX/token transfer needs to be handled via post-conditions
                    ;; or the contract needs to be updated to use as-contract when available
                    (ok withdraw-amount)
                )
            )
        )
    )
)

;; Read-only functions

;; Get balance for a user and token
(define-read-only (get-balance (token principal) (owner principal))
    (ok (default-to u0 (map-get? balances { token: token, owner: owner })))
)

;; Get lock information for a user
(define-read-only (get-lock-info (owner principal))
    (ok (map-get? lock-info { owner: owner }))
)

;; Check if lock period has expired
(define-read-only (is-lock-expired (owner principal))
    (let ((lock (map-get? lock-info { owner: owner })))
        (if (is-some lock)
            (let ((lock-data (unwrap-panic lock))
                  (lock-start (get lock-start lock-data))
                  (lock-duration (get lock-duration lock-data))
                  (elapsed (- stacks-block-height lock-start)))
                (ok (>= elapsed lock-duration))
            )
            (ok false)
        )
    )
)

;; Get remaining lock blocks
(define-read-only (get-remaining-lock-blocks (owner principal))
    (let ((lock (map-get? lock-info { owner: owner })))
        (if (is-some lock)
            (let ((lock-data (unwrap-panic lock))
                  (lock-start (get lock-start lock-data))
                  (lock-duration (get lock-duration lock-data))
                  (elapsed (- stacks-block-height lock-start)))
                (if (>= elapsed lock-duration)
                    (ok u0)
                    (ok (- lock-duration elapsed))
                )
            )
            (ok u0)
        )
    )
)

Functions (8)

FunctionAccessArgs
deposit-stxpublicamount: uint
deposit-tokenpublicamount: uint, token: <sip010-token>
set-lock-durationpublicduration: uint
withdrawpublicamount: uint
get-balanceread-onlytoken: principal, owner: principal
get-lock-inforead-onlyowner: principal
is-lock-expiredread-onlyowner: principal
get-remaining-lock-blocksread-onlyowner: principal