Source Code

;; title: StackerMint
;; version:
;; summary:
;; description:

;; Constants
(define-constant CONTRACT-OWNER tx-sender)
(define-constant ERR-NOT-AUTHORIZED (err u1001))
(define-constant ERR-VESTING-NOT-FOUND (err u1002))
(define-constant ERR-NO-TOKENS-AVAILABLE (err u1003))
(define-constant ERR-VESTING-ALREADY-EXISTS (err u1004))
(define-constant ERR-INVALID-SCHEDULE (err u1005))
(define-constant ERR-VESTING-NOT-STARTED (err u1006))

;; Data variables
(define-data-var total-vesting-schedules uint u0)

;; Data maps
(define-map vesting-schedules
    { beneficiary: principal, schedule-id: uint }
    {
        total-amount: uint,
        released-amount: uint,
        start-time: uint,                   ;; Clarity 4: Unix timestamp
        cliff-time: uint,                   ;; Clarity 4: Unix timestamp
        end-time: uint,                     ;; Clarity 4: Unix timestamp
        revocable: bool,
        revoked: bool,
        revoked-at: (optional uint)         ;; Clarity 4: Unix timestamp
    }
)

(define-map beneficiary-schedule-count { beneficiary: principal } uint)

(define-map vesting-releases
    { beneficiary: principal, schedule-id: uint, release-index: uint }
    {
        amount: uint,
        released-at: uint                   ;; Clarity 4: Unix timestamp
    }
)

(define-map release-count { beneficiary: principal, schedule-id: uint } uint)

;; Read-only functions
(define-read-only (get-vesting-schedule (beneficiary principal) (schedule-id uint))
    (ok (map-get? vesting-schedules { beneficiary: beneficiary, schedule-id: schedule-id }))
)

(define-read-only (get-beneficiary-schedule-count (beneficiary principal))
    (ok (default-to u0 (map-get? beneficiary-schedule-count { beneficiary: beneficiary })))
)

(define-read-only (get-vesting-release (beneficiary principal) (schedule-id uint) (release-index uint))
    (ok (map-get? vesting-releases { beneficiary: beneficiary, schedule-id: schedule-id, release-index: release-index }))
)

(define-read-only (calculate-vested-amount (beneficiary principal) (schedule-id uint))
    (match (map-get? vesting-schedules { beneficiary: beneficiary, schedule-id: schedule-id })
        schedule
            (if (get revoked schedule)
                (ok u0)
                (if (< stacks-block-time (get cliff-time schedule))
                    (ok u0)
                    (if (>= stacks-block-time (get end-time schedule))
                        (ok (get total-amount schedule))
                        (let
                            (
                                (elapsed (- stacks-block-time (get start-time schedule)))
                                (duration (- (get end-time schedule) (get start-time schedule)))
                                (vested (/ (* (get total-amount schedule) elapsed) duration))
                            )
                            (ok vested)
                        )
                    )
                )
            )
        ERR-VESTING-NOT-FOUND
    )
)

(define-read-only (calculate-releasable-amount (beneficiary principal) (schedule-id uint))
    (match (map-get? vesting-schedules { beneficiary: beneficiary, schedule-id: schedule-id })
        schedule
            (let
                (
                    (vested (unwrap! (calculate-vested-amount beneficiary schedule-id) ERR-VESTING-NOT-FOUND))
                    (released (get released-amount schedule))
                )
                (ok (if (> vested released) (- vested released) u0))
            )
        ERR-VESTING-NOT-FOUND
    )
)

;; Public functions
(define-public (create-vesting-schedule
    (beneficiary principal)
    (total-amount uint)
    (start-time uint)
    (cliff-duration uint)
    (vesting-duration uint)
    (revocable bool))
    (begin
        (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED)
        (asserts! (> total-amount u0) ERR-INVALID-SCHEDULE)
        (asserts! (> vesting-duration u0) ERR-INVALID-SCHEDULE)
        (asserts! (>= start-time stacks-block-time) ERR-INVALID-SCHEDULE)

        (let
            (
                (schedule-id (default-to u0 (map-get? beneficiary-schedule-count { beneficiary: beneficiary })))
                (cliff-time (+ start-time cliff-duration))
                (end-time (+ start-time vesting-duration))
            )
            (map-set vesting-schedules
                { beneficiary: beneficiary, schedule-id: schedule-id }
                {
                    total-amount: total-amount,
                    released-amount: u0,
                    start-time: start-time,
                    cliff-time: cliff-time,
                    end-time: end-time,
                    revocable: revocable,
                    revoked: false,
                    revoked-at: none
                }
            )

            (map-set beneficiary-schedule-count { beneficiary: beneficiary } (+ schedule-id u1))
            (var-set total-vesting-schedules (+ (var-get total-vesting-schedules) u1))

            (print {
                event: "vesting-schedule-created",
                beneficiary: beneficiary,
                schedule-id: schedule-id,
                total-amount: total-amount,
                start-time: start-time,
                cliff-time: cliff-time,
                end-time: end-time,
                timestamp: stacks-block-time
            })
            (ok schedule-id)
        )
    )
)

(define-public (release-vested-tokens (schedule-id uint))
    (let
        (
            (schedule (unwrap! (map-get? vesting-schedules { beneficiary: tx-sender, schedule-id: schedule-id }) ERR-VESTING-NOT-FOUND))
            (releasable (unwrap! (calculate-releasable-amount tx-sender schedule-id) ERR-VESTING-NOT-FOUND))
        )
        (asserts! (not (get revoked schedule)) ERR-NOT-AUTHORIZED)
        (asserts! (>= stacks-block-time (get cliff-time schedule)) ERR-VESTING-NOT-STARTED)
        (asserts! (> releasable u0) ERR-NO-TOKENS-AVAILABLE)

        (let
            (
                (release-index (default-to u0 (map-get? release-count { beneficiary: tx-sender, schedule-id: schedule-id })))
                (new-released (+ (get released-amount schedule) releasable))
            )
            ;; Update vesting schedule
            (map-set vesting-schedules
                { beneficiary: tx-sender, schedule-id: schedule-id }
                (merge schedule { released-amount: new-released })
            )

            ;; Record release
            (map-set vesting-releases
                { beneficiary: tx-sender, schedule-id: schedule-id, release-index: release-index }
                {
                    amount: releasable,
                    released-at: stacks-block-time
                }
            )

            (map-set release-count { beneficiary: tx-sender, schedule-id: schedule-id } (+ release-index u1))

            (print {
                event: "tokens-released",
                beneficiary: tx-sender,
                schedule-id: schedule-id,
                amount: releasable,
                timestamp: stacks-block-time
            })
            (ok releasable)
        )
    )
)

(define-public (revoke-vesting (beneficiary principal) (schedule-id uint))
    (let
        (
            (schedule (unwrap! (map-get? vesting-schedules { beneficiary: beneficiary, schedule-id: schedule-id }) ERR-VESTING-NOT-FOUND))
        )
        (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED)
        (asserts! (get revocable schedule) ERR-NOT-AUTHORIZED)
        (asserts! (not (get revoked schedule)) ERR-NOT-AUTHORIZED)

        (map-set vesting-schedules
            { beneficiary: beneficiary, schedule-id: schedule-id }
            (merge schedule {
                revoked: true,
                revoked-at: (some stacks-block-time)
            })
        )

        (print {
            event: "vesting-revoked",
            beneficiary: beneficiary,
            schedule-id: schedule-id,
            timestamp: stacks-block-time
        })
        (ok true)
    )
)

(define-public (batch-create-vesting (beneficiaries (list 50 { recipient: principal, amount: uint })) (start-time uint) (cliff-duration uint) (vesting-duration uint))
    (begin
        (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED)
        (ok (map create-vesting-helper beneficiaries))
    )
)

(define-private (create-vesting-helper (beneficiary-data { recipient: principal, amount: uint }))
    (unwrap-panic (create-vesting-schedule
        (get recipient beneficiary-data)
        (get amount beneficiary-data)
        stacks-block-time
        u2592000  ;; 30 days cliff
        u31536000 ;; 365 days vesting
        true
    ))
)

Functions (10)

FunctionAccessArgs
get-vesting-scheduleread-onlybeneficiary: principal, schedule-id: uint
get-beneficiary-schedule-countread-onlybeneficiary: principal
get-vesting-releaseread-onlybeneficiary: principal, schedule-id: uint, release-index: uint
calculate-vested-amountread-onlybeneficiary: principal, schedule-id: uint
calculate-releasable-amountread-onlybeneficiary: principal, schedule-id: uint
create-vesting-schedulepublicbeneficiary: principal, total-amount: uint, start-time: uint, cliff-duration: uint, vesting-duration: uint, revocable: bool
release-vested-tokenspublicschedule-id: uint
revoke-vestingpublicbeneficiary: principal, schedule-id: uint
batch-create-vestingpublicbeneficiaries: (list 50 { recipient: principal, amount: uint }
create-vesting-helperprivatebeneficiary-data: { recipient: principal, amount: uint }