;;
;; fak.fun PRE @version 1.0
;; Pre-launch contract for token distribution
;; Dynamic allocation: 1-7 seats per user in Period 1
;; Each seat = 0.00069000 BTC, targeting 20 seats total with minimum 10 users
;; Pre-launch participants are activating the DEX contract
;; and therefore can claim fees generated by DEX
;; Fee airdrops are separate from token purchase and vesting.
(use-trait faktory-token 'SP3XXMS38VTAWTVPE5682XSBFXPTH7XCPEBTX8AN2.faktory-trait-v1.sip-010-trait)
(define-constant SEATS u20)
(define-constant MIN-USERS u10)
(define-constant MAX-SEATS-PER-USER u7)
(define-constant PRICE-PER-SEAT u69000) ;; 69K sats per seat
(define-constant TOKENS-PER-SEAT u1000000000000000) ;; 10M tokens per seat if supply 1B
(define-constant DEX-AMOUNT u1000000)
(define-constant MULTI-SIG-AMOUNT u30000)
(define-constant FEE-AMOUNT u350000)
(define-constant FT-INITIALIZED-BALANCE u20000000000000000) ;; 200M tokens for pre-launch if supply 1B
;; Vesting schedule (percentages add up to 100)
(define-constant VESTING-SCHEDULE
(list
;; Initial release - 10% at once
{height: u100, percent: u10, id: u0} ;; 10% at initial unlock
;; Second phase - 20% total across 6 drips
{height: u250, percent: u3, id: u1} ;; 3%
{height: u400, percent: u3, id: u2} ;; 3%
{height: u550, percent: u3, id: u3} ;; 3%
{height: u700, percent: u3, id: u4} ;; 3%
{height: u850, percent: u4, id: u5} ;; 4%
{height: u1000, percent: u4, id: u6} ;; 4% - hitting 30% total at original second milestone
;; Third phase - 30% total across 7 drips
{height: u1200, percent: u4, id: u7} ;; 4%
{height: u1400, percent: u4, id: u8} ;; 4%
{height: u1600, percent: u4, id: u9} ;; 4%
{height: u1750, percent: u4, id: u10} ;; 4%
{height: u1900, percent: u4, id: u11} ;; 4%
{height: u2000, percent: u5, id: u12} ;; 5%
{height: u2100, percent: u5, id: u13} ;; 5% - hitting 60% total at original third milestone
;; Final phase - 40% total across 7 drips
{height: u2500, percent: u5, id: u14} ;; 5%
{height: u2900, percent: u5, id: u15} ;; 5%
{height: u3300, percent: u6, id: u16} ;; 6%
{height: u3600, percent: u6, id: u17} ;; 6%
{height: u3900, percent: u6, id: u18} ;; 6%
{height: u4100, percent: u6, id: u19} ;; 6%
{height: u4200, percent: u6, id: u20})) ;; 6% - hitting 100% total at original final milestone
(define-constant FAKTORY1 'SMH8FRN30ERW1SX26NJTJCKTDR3H27NRJ6W75WQE)
(define-constant FAKTORY2 'SM362G0X1YNB2M3FWWFAASV9WB3XHQ8RWP512SSX3)
;; Data vars
(define-data-var ft-balance uint u0)
(define-data-var stx-balance uint u0)
(define-data-var total-seats-taken uint u0)
(define-data-var total-users uint u0)
(define-data-var distribution-height uint u0)
(define-data-var deployment-height uint burn-block-height)
(define-data-var accelerated-vesting bool false)
(define-data-var market-open bool false)
(define-data-var governance-active bool false)
(define-data-var acc-distributed uint u0)
;; Token, Dex and sBTC
(define-constant TOKEN-DAO 'SPV9K21TBFAK4KNRJXF5DFP8N7W46G4V9RCJDC22.nono-faktory) ;; param
(define-constant DEX-DAO 'SPV9K21TBFAK4KNRJXF5DFP8N7W46G4V9RCJDC22.nono-faktory-dex) ;; param
;; Define a data variable to track seat holders
(define-data-var seat-holders (list 20 {owner: principal, seats: uint}) (list))
;; Track seat ownership and claims
(define-map seats-owned principal uint)
(define-map claimed-amounts principal uint)
;; Error constants
(define-constant ERR-NO-SEATS-LEFT (err u301))
(define-constant ERR-NOT-SEAT-OWNER (err u302))
(define-constant ERR-NOTHING-TO-CLAIM (err u304))
(define-constant ERR-NOT-AUTHORIZED (err u305))
(define-constant ERR-WRONG-TOKEN (err u307))
(define-constant ERR-CONTRACT-INSUFFICIENT-FUNDS (err u311))
(define-constant ERR-INVALID-SEAT-COUNT (err u313))
(define-constant ERR-REMOVING-HOLDER (err u316))
(define-constant ERR-DISTRIBUTION-ALREADY-SET (err u320))
(define-constant ERR-DISTRIBUTION-NOT-INITIALIZED (err u321))
;; Helper function to update seat holders list
(define-private (update-seat-holder (owner principal) (seat-count uint))
(let ((current-holders (var-get seat-holders))
(updated-list (update-or-add-holder current-holders owner seat-count)))
(var-set seat-holders updated-list)))
;; Helper to update or add a holder to the list
(define-private (update-or-add-holder
(holders (list 20 {owner: principal, seats: uint}))
(owner principal) (seat-count uint))
(let ((position (find-holder-position holders)))
(if (is-some position)
;; Update existing holder - unwrap the optional result
(unwrap-panic (replace-at? holders (unwrap-panic position) {owner: owner, seats: seat-count}))
;; Add new holder
(unwrap-panic (as-max-len? (append holders {owner: owner, seats: seat-count}) u20)))))
;; Helper to find a holder's position in the list
(define-private (find-holder-position
(holders (list 20 {owner: principal, seats: uint})))
(let ((result (fold check-if-owner
holders
{found: false, index: u0})))
(if (get found result)
(some (get index result))
none)))
(define-private (check-if-owner
(entry {owner: principal, seats: uint})
(state {found: bool, index: uint}))
(if (get found state)
;; Already found, just pass through
state
;; Check if this is the owner we're looking for
(if (is-eq (get owner entry) tx-sender)
;; Found it, update state
{found: true, index: (get index state)}
;; Not found, increment counter
{found: false, index: (+ (get index state) u1)})))
(define-private (remove-seat-holder)
(let ((filtered-list (filter not-matching-owner (var-get seat-holders))))
(var-set seat-holders filtered-list)
(ok true)))
(define-private (not-matching-owner (entry {owner: principal, seats: uint}))
(not (is-eq (get owner entry) tx-sender)))
;; Main functions
;; Buy seats in Period 1
(define-public (buy-up-to (seat-count uint))
(let (
(current-seats (var-get total-seats-taken))
(user-seats (default-to u0 (map-get? seats-owned tx-sender)))
(max-total-allowed (get-max-seats-allowed))
(max-additional-allowed (if (>= (var-get total-users) MIN-USERS)
max-total-allowed ;; if quota of users is attained, anyone can buy
(if (>= user-seats max-total-allowed)
u0
(- max-total-allowed user-seats))))
(actual-seats (if (> seat-count max-additional-allowed)
max-additional-allowed
seat-count)))
(asserts! (is-eq (var-get distribution-height) u0) ERR-DISTRIBUTION-ALREADY-SET)
(asserts! (> actual-seats u0) ERR-INVALID-SEAT-COUNT)
(asserts! (<= (+ user-seats actual-seats) MAX-SEATS-PER-USER) ERR-INVALID-SEAT-COUNT)
(asserts! (< current-seats SEATS) ERR-NO-SEATS-LEFT)
;; Process payment
(match (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token
transfer (* PRICE-PER-SEAT actual-seats) tx-sender (as-contract tx-sender) none)
success
(begin
(if (is-eq user-seats u0)
(var-set total-users (+ (var-get total-users) u1))
true)
(map-set seats-owned tx-sender (+ user-seats actual-seats))
(var-set total-seats-taken (+ current-seats actual-seats))
(var-set stx-balance (+ (var-get stx-balance) (* PRICE-PER-SEAT actual-seats)))
(update-seat-holder tx-sender (+ user-seats actual-seats))
(if (and (>= (var-get total-users) MIN-USERS) ;; Check if we should set distribution height
(>= (var-get total-seats-taken) SEATS))
(try! (initialize-token-distribution))
true)
(print {
type: "buy-seats",
buyer: tx-sender,
seats-owned: (+ user-seats actual-seats),
total-users: (var-get total-users),
total-seats-taken: (+ current-seats actual-seats),
stx-balance: (var-get stx-balance),
seat-holders: (var-get seat-holders),
distribution-height: (var-get distribution-height)
})
(ok true))
error (err error))))
;; Refund logic only for Period 1 expired and Period 2 not started
(define-public (refund)
(let (
(user-seats (default-to u0 (map-get? seats-owned tx-sender)))
(seat-owner tx-sender))
(asserts! (is-eq (var-get distribution-height) u0) ERR-DISTRIBUTION-ALREADY-SET)
(asserts! (> user-seats u0) ERR-NOT-SEAT-OWNER)
;; Process refund
(match (as-contract (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token
transfer (* PRICE-PER-SEAT user-seats) tx-sender seat-owner none))
success
(let ((is-removed (unwrap! (remove-seat-holder) ERR-REMOVING-HOLDER)))
(map-delete seats-owned tx-sender)
(var-set total-seats-taken (- (var-get total-seats-taken) user-seats))
(var-set total-users (- (var-get total-users) u1))
(var-set stx-balance (- (var-get stx-balance) (* PRICE-PER-SEAT user-seats)))
(print {
type: "refund",
user: tx-sender,
seats-refunded: user-seats,
seat-holders: (var-get seat-holders),
total-seats-taken: (var-get total-seats-taken),
total-users: (var-get total-users),
stx-balance: (var-get stx-balance)
})
(ok true))
error (err error))))
(define-private (get-claimable-amount (owner principal))
(begin
(if (> (var-get distribution-height) u0)
(let ((claimed (default-to u0 (map-get? claimed-amounts owner)))
(seats-owner (default-to u0 (map-get? seats-owned owner)))
(vested (fold check-claimable VESTING-SCHEDULE u0)))
(- (* vested seats-owner) claimed)) ;; double claiming is impossible
u0))) ;; If distribution not initialized (equals u0), nothing is claimable
(define-private (check-claimable (entry {height: uint, percent: uint, id: uint}) (current-total uint))
(let ((distribution-start (var-get distribution-height)))
(if (<= (+ distribution-start (get height entry)) burn-block-height)
(+ current-total (/ (* TOKENS-PER-SEAT (get percent entry)) u100))
(if (and
(var-get accelerated-vesting) ;; token graduated, accelerated vesting
(<= (get id entry) u13)) ;; we're in first 13 entries (0,1,2)
(+ current-total (/ (* TOKENS-PER-SEAT (get percent entry)) u100))
current-total))))
;; Claim vested tokens
(define-public (claim (ft <faktory-token>))
(let ((claimable (get-claimable-amount tx-sender))
(seat-owner tx-sender))
(asserts! (> (var-get distribution-height) u0) ERR-DISTRIBUTION-NOT-INITIALIZED)
(asserts! (is-eq (contract-of ft) TOKEN-DAO) ERR-WRONG-TOKEN)
(asserts! (> (default-to u0 (map-get? seats-owned tx-sender)) u0) ERR-NOT-SEAT-OWNER)
(asserts! (> claimable u0) ERR-NOTHING-TO-CLAIM)
(asserts! (>= (var-get ft-balance) claimable) ERR-CONTRACT-INSUFFICIENT-FUNDS)
(match (as-contract (contract-call? ft transfer claimable tx-sender seat-owner none))
success
(begin
(map-set claimed-amounts tx-sender
(+ (default-to u0 (map-get? claimed-amounts tx-sender)) claimable))
(var-set ft-balance (- (var-get ft-balance) claimable)) ;; reduce ft-balance by claimable
(print {
type: "claim",
user: tx-sender,
amount-claimed: claimable,
total-claimed: (map-get? claimed-amounts tx-sender),
ft-balance: (var-get ft-balance)
})
(ok claimable))
error (err error))))
;; Claim vested tokens on behalf of a specific holder
(define-public (claim-on-behalf (ft <faktory-token>) (holder principal))
(let ((claimable (get-claimable-amount holder)))
(asserts! (> (var-get distribution-height) u0) ERR-DISTRIBUTION-NOT-INITIALIZED)
(asserts! (is-eq (contract-of ft) TOKEN-DAO) ERR-WRONG-TOKEN)
(asserts! (> (default-to u0 (map-get? seats-owned holder)) u0) ERR-NOT-SEAT-OWNER)
(asserts! (> claimable u0) ERR-NOTHING-TO-CLAIM)
(asserts! (>= (var-get ft-balance) claimable) ERR-CONTRACT-INSUFFICIENT-FUNDS)
(match (as-contract (contract-call? ft transfer claimable tx-sender holder none))
success
(begin
(map-set claimed-amounts holder
(+ (default-to u0 (map-get? claimed-amounts holder)) claimable))
(var-set ft-balance (- (var-get ft-balance) claimable))
(print {
type: "claim",
user: holder,
amount-claimed: claimable,
total-claimed: (map-get? claimed-amounts holder),
ft-balance: (var-get ft-balance)
})
(ok claimable))
error (err error))))
;; Read only functions
(define-read-only (get-max-seats-allowed)
(let (
(total-users-now (var-get total-users))
(seats-remaining (- SEATS (var-get total-seats-taken)))
;; Check if we've already met the minimum users requirement
(users-remaining (if (>= total-users-now MIN-USERS)
u0 ;; No more users needed
(- MIN-USERS total-users-now)))
(max-possible (if (>= total-users-now MIN-USERS)
seats-remaining ;; No more users needed
(+ (- seats-remaining users-remaining) u1))))
(if (>= max-possible MAX-SEATS-PER-USER)
MAX-SEATS-PER-USER
max-possible)))
(define-read-only (get-contract-status)
(ok
{
is-distribution-period: (> (var-get distribution-height) u0),
total-users: (var-get total-users),
total-seats-taken: (var-get total-seats-taken),
deployment-height: (var-get deployment-height),
distribution-height: (var-get distribution-height),
accelerated-vesting: (var-get accelerated-vesting),
market-open: (var-get market-open),
governance-active: (var-get governance-active),
seat-holders: (var-get seat-holders)
}))
(define-read-only (get-user-info (user principal))
(ok
{
seats-owned: (default-to u0 (map-get? seats-owned user)),
amount-claimed: (default-to u0 (map-get? claimed-amounts user)),
claimable-amount: (get-claimable-amount user)
}))
(define-read-only (get-remaining-seats)
(ok {remainin-seats: (- SEATS (var-get total-seats-taken))}))
(define-read-only (get-seats-owned (address principal))
(ok {seats-owned: (default-to u0 (map-get? seats-owned address))}))
(define-read-only (get-claimed-amount (address principal))
(ok {claimed-amount:
(default-to u0 (map-get? claimed-amounts address))}))
(define-read-only (get-vesting-schedule)
(ok {vesting-schedule: VESTING-SCHEDULE}))
(define-read-only (get-seat-holders)
(ok {seat-holders: (var-get seat-holders)}))
(define-read-only (is-market-open)
(ok (var-get market-open))
)
(define-read-only (is-governance-active)
(ok (var-get governance-active))
)
;; on pre-launch successful completion
(define-private (initialize-token-distribution)
(begin
(var-set market-open true)
(var-set governance-active true) ;; jason: in core/action proposal, checks from create proposal, deadlocks anything happening
(try! (as-contract (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token
transfer DEX-AMOUNT tx-sender DEX-DAO none))) ;; BTC to DEX
(try! (as-contract (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token
transfer MULTI-SIG-AMOUNT tx-sender FAKTORY1 none)))
(try! (as-contract (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token
transfer FEE-AMOUNT tx-sender FAKTORY2 none)))
(var-set distribution-height burn-block-height)
(var-set last-airdrop-height (some burn-block-height))
(var-set ft-balance FT-INITIALIZED-BALANCE)
(var-set stx-balance u0)
(print {
type: "distribution-initialized",
token-contract: TOKEN-DAO,
distribution-height: burn-block-height,
ft-balance: FT-INITIALIZED-BALANCE
})
(ok true)))
;; on Bonding
(define-public (toggle-bonded)
(begin
(asserts! (is-eq contract-caller DEX-DAO) ERR-NOT-AUTHORIZED)
(var-set accelerated-vesting true)
(var-set final-airdrop-mode true)
(print {
type: "bonded",
token-contract: TOKEN-DAO,
bonded-height: burn-block-height,
})
(ok true)))
;; Simplified Fee Distribution System
;; Constants
(define-constant COOLDOWN-PERIOD u2100) ;; Longer cooldown between airdrops
;; Error constants
(define-constant ERR-NO-FEES-TO-DISTRIBUTE (err u323))
(define-constant ERR-COOLDOWN-ACTIVE (err u324))
(define-constant ERR-TOTAL-SEATS-ZERO (err u325))
;; Data vars for fee tracking
(define-data-var accumulated-fees uint u0) ;; Total fees accumulated since last airdrop
(define-data-var last-airdrop-height (optional uint) (some u0)) ;; Block height of the last airdrop
(define-data-var final-airdrop-mode bool false) ;; Toggle for final airdrop mode
;; Add this function to allow the DEX to send fees to the contract
(define-public (create-fees-receipt (amount uint))
(let ((current-fees (var-get accumulated-fees)))
;; Only the DEX contract can call this function
(asserts! (is-eq contract-caller DEX-DAO) ERR-NOT-AUTHORIZED)
;; Update accumulated fees
(var-set accumulated-fees (+ current-fees amount))
(print {
type: "fees-received",
amount: amount,
total-accumulated: (+ current-fees amount)
})
(ok true)))
;; Check if airdrop can be triggered
(define-read-only (can-trigger-airdrop)
(let ((cooldown-expired (>= burn-block-height (+ (unwrap-panic (var-get last-airdrop-height)) COOLDOWN-PERIOD)))
(has-fees (> (var-get accumulated-fees) u0))
(final-mode (var-get final-airdrop-mode)))
(or (and cooldown-expired has-fees)
(and final-mode has-fees))))
;; Main airdrop function - anyone can call
(define-public (trigger-fee-airdrop)
(let ((total-fees (var-get accumulated-fees))
(total-seats (var-get total-seats-taken))
(can-airdrop (can-trigger-airdrop))
(acc-fees (var-get accumulated-fees)))
;; Check if airdrop can be triggered
(asserts! can-airdrop (if (> total-fees u0)
ERR-COOLDOWN-ACTIVE
ERR-NO-FEES-TO-DISTRIBUTE))
;; Must have fees to distribute and seats must exist
(asserts! (> total-fees u0) ERR-NO-FEES-TO-DISTRIBUTE)
(asserts! (> total-seats u0) ERR-TOTAL-SEATS-ZERO)
;; Distribute fees to all seat holders
(var-set acc-distributed u0)
(let ((distributions (map distribute-to-holder (var-get seat-holders))))
(if (> (- acc-fees (var-get acc-distributed)) u0)
(match (as-contract (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token
transfer (- acc-fees (var-get acc-distributed)) tx-sender FAKTORY1 none))
success
(begin
(print {
type: "fee-residual",
recipient: FAKTORY1,
amount: (- acc-fees (var-get acc-distributed))
})
true)
error false)
true)
;; Reset accumulated fees and update last airdrop height
(var-set accumulated-fees u0)
(var-set last-airdrop-height (some burn-block-height))
(print {
type: "fee-airdrop",
total-distributed: total-fees,
timestamp: burn-block-height,
distributions: distributions
})
(ok total-fees))))
;; Helper function to distribute fees to a single holder
(define-private (distribute-to-holder (entry {owner: principal, seats: uint}))
(let ((holder (get owner entry))
(user-seats (get seats entry))
(total-seats (var-get total-seats-taken))
(total-fees (var-get accumulated-fees))
(user-share (if (and (> user-seats u0) (> total-seats u0))
(/ (* total-fees user-seats) total-seats)
u0)))
;; Only distribute if the user's share is greater than zero
(if (> user-share u0)
(match (as-contract (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token
transfer user-share tx-sender holder none))
success
(begin
(var-set acc-distributed (+ (var-get acc-distributed) user-share))
{recipient: holder, amount: user-share})
error {recipient: holder, amount: u0})
{recipient: holder, amount: u0})))
;; Helper to extract principal from seat-holder entry
(define-private (get-holder-principal (entry {owner: principal, seats: uint}))
(get owner entry))
;; Get all unique seat holders
(define-read-only (get-all-seat-holders)
(ok (var-get seat-holders)))
;; Get fee distribution info for UI
(define-read-only (get-fee-distribution-info)
(ok {
accumulated-fees: (var-get accumulated-fees),
last-airdrop-height: (var-get last-airdrop-height),
current-height: burn-block-height,
cooldown-period: COOLDOWN-PERIOD,
final-airdrop-mode: (var-get final-airdrop-mode),
can-trigger-now: (can-trigger-airdrop)
}))
;; Get user's expected share in the next airdrop
(define-read-only (get-user-expected-share (user principal))
(let ((user-seats (default-to u0 (map-get? seats-owned user)))
(total-seats (var-get total-seats-taken))
(total-fees (var-get accumulated-fees)))
(ok {
user: user,
user-seats: user-seats,
total-seats: total-seats,
total-accumulated-fees: total-fees,
expected-share: (if (and (> user-seats u0) (> total-seats u0) (> total-fees u0))
(/ (* total-fees user-seats) total-seats)
u0)
})))