;; title: payment-vm
;; version:
;; summary:
;; description:
;; traits
;;
;; token definitions
;;
;; --- Read Only Functions ---
;; @desc Get core protocol configuration and status
(define-read-only (get-protocol-status)
(ok {
version: CONTRACT-VERSION,
paused: (var-get is-paused),
owner: (var-get contract-owner),
fee-percentage: (var-get fee-percentage),
total-volume: (var-get total-volume),
total-fees-collected: (var-get total-fees-collected),
require-verification: (var-get require-merchant-verification)
})
)
;; @desc Retrieve full user data by principal
;; @param account principal - The user account to lookup
(define-read-only (get-user-data (account principal))
(map-get? Users account)
)
;; @desc Retrieve user profile metadata
;; @param account principal - The user account to lookup
(define-read-only (get-user-profile (account principal))
(map-get? UserProfiles account)
)
;; @desc Retrieve full merchant data by principal
;; @param account principal - The merchant account to lookup
(define-read-only (get-merchant-data (account principal))
(map-get? Merchants account)
)
;; @desc Search for user principal by username
;; @param username (string-ascii 24) - The unique username
(define-read-only (get-principal-by-username (username (string-ascii 24)))
(map-get? Usernames username)
)
;; @desc Inspect a specific system event
;; @param id uint - The event id
(define-read-only (get-system-event (id uint))
(map-get? SystemEvents { event-id: id })
)
;; @desc Helper to check if a user is an active merchant
;; @param account principal - The account to check
(define-read-only (is-active-merchant (account principal))
(let (
(merchant (map-get? Merchants account))
)
(match merchant
profile (is-eq (get status profile) "verified")
false
)
)
)
;; @desc Get current system nonces
(define-read-only (get-nonces)
{
users: (var-get user-nonce),
merchants: (var-get merchant-nonce),
events: (var-get event-nonce)
}
)
;; @desc Get total registered user count
(define-read-only (get-total-users)
(ok (var-get user-nonce))
)
;; Error Constants
(define-constant ERR-UNAUTHORIZED (err u100))
(define-constant ERR-NOT-OWNER (err u101))
(define-constant ERR-ALREADY-REGISTERED (err u102))
(define-constant ERR-USER-NOT-FOUND (err u103))
(define-constant ERR-MERCHANT-NOT-FOUND (err u104))
(define-constant ERR-INVALID-AMOUNT (err u105))
(define-constant ERR-INSUFFICIENT-FUNDS (err u106))
(define-constant ERR-PAYMENT-EXPIRED (err u107))
(define-constant ERR-ALREADY-PAID (err u108))
(define-constant ERR-INVALID-STATUS (err u109))
(define-constant ERR-CONTRACT-PAUSED (err u110))
(define-constant ERR-INVALID-FEE (err u111))
(define-constant ERR-LIMIT-EXCEEDED (err u112))
(define-constant ERR-INVALID-PRINCIPAL (err u113))
(define-constant ERR-SUBSCRIPTION-EXPIRED (err u114))
(define-constant ERR-PLAN-NOT-FOUND (err u115))
(define-constant ERR-NOT-ALLOWED (err u116))
;; Configuration Constants
(define-constant BP-PERCENT u10000) ;; Basis points (100% = 10000)
(define-constant MAX-FEE-PERCENT u500) ;; Max fee 5% (500 basis points)
(define-constant MIN-PAYMENT-STX u1000000) ;; 1 STX minimum for logic-heavy payments
(define-constant DISPUTE-WINDOW-BLOCKS u144) ;; ~24 hours
(define-constant REGISTRATION-FEE-STX u5000000) ;; 5 STX onboarding fee
(define-constant MERCHANT-VERIFICATION-STAKE u50000000) ;; 50 STX stake for high-tier merchants
;; Operational Variables
(define-constant CONTRACT-VERSION "1.1.0")
(define-constant PLATFORM-TAG "SPAY-PROTO")
;; --- Data Variables ---
;; The primary administrator address that can update protocol settings
(define-data-var contract-owner principal tx-sender)
;; A safety switch to halt sensitive operations in case of emergency
(define-data-var is-paused bool false)
;; The treasury address where platform fees are directed
(define-data-var fee-receiver principal tx-sender)
;; Current platform fee in basis points (e.g., 200 = 2%)
(define-data-var fee-percentage uint u200)
;; Scaling counter for user registrations to ensure unique indexing
(define-data-var user-nonce uint u0)
;; Scaling counter for merchant registrations and profile tracking
(define-data-var merchant-nonce uint u0)
;; Total transaction volume processed by the protocol in micro-STX
(define-data-var total-volume uint u0)
;; Total platform fees collected since deployment
(define-data-var total-fees-collected uint u0)
;; Global switch for merchant verification requirements
(define-data-var require-merchant-verification bool true)
;; Maximum allowed transaction volume per single payment
(define-data-var max-payment-amount (optional uint) none)
;; --- Data Maps ---
;; User Records - maps principal to a unique user-id and registration data
(define-map Users
principal
{
user-id: uint,
username: (string-ascii 24),
status: (string-ascii 12), ;; "active", "suspended"
registered-at: uint,
total-spent: uint,
total-received: uint,
is-merchant: bool
}
)
;; Username Lookup - maps username to principal to ensure uniqueness
(define-map Usernames
(string-ascii 24)
principal
)
;; --- Public Functions ---
;; @desc Register a new user in the protocol
;; @param username (string-ascii 24) - A unique display name for the user
(define-public (register-user (username (string-ascii 24)))
(let (
(new-id (+ (var-get user-nonce) u1))
)
;; Check if contract is paused
(asserts! (not (var-get is-paused)) ERR-CONTRACT-PAUSED)
;; Check if user is already registered
(asserts! (is-none (map-get? Users tx-sender)) ERR-ALREADY-REGISTERED)
;; Check if username is already taken
(asserts! (is-none (map-get? Usernames username)) ERR-ALREADY-REGISTERED)
;; Optional: Handle registration fee if enabled
(if (> REGISTRATION-FEE-STX u0)
(try! (stx-transfer? REGISTRATION-FEE-STX tx-sender (var-get fee-receiver)))
true
)
;; Store user record
(map-set Users tx-sender {
user-id: new-id,
username: username,
status: "active",
registered-at: block-height,
total-spent: u0,
total-received: u0,
is-merchant: false
})
;; Map username to principal
(map-set Usernames username tx-sender)
;; Increment nonce
(var-set user-nonce new-id)
;; Emit registration event
(print {
event: "user-registered",
user: tx-sender,
user-id: new-id,
username: username
})
(ok new-id)
)
)
;; Merchant Records - maps principal to their business details
(define-map Merchants
principal
{
merchant-id: uint,
business-name: (string-ascii 64),
description: (string-ascii 256),
website: (string-ascii 128),
status: (string-ascii 12), ;; "pending", "verified", "suspended"
tier: (string-ascii 12), ;; "basic", "premium", "enterprise"
total-revenue: uint,
dispute-count: uint,
metadata-hash: (buff 32)
}
)
;; --- Public Functions ---
;; @desc Register as a merchant to receive professional payments
;; @param business-name (string-ascii 64) - The legal or brand name
;; @param website (string-ascii 128) - Official business website
(define-public (register-merchant (business-name (string-ascii 64)) (website (string-ascii 128)))
(let (
(user (unwrap! (map-get? Users tx-sender) ERR-USER-NOT-FOUND))
(new-id (+ (var-get merchant-nonce) u1))
)
(begin
;; Check if contract is paused
(asserts! (not (var-get is-paused)) ERR-CONTRACT-PAUSED)
;; Check if already a merchant
(asserts! (is-none (map-get? Merchants tx-sender)) ERR-ALREADY-REGISTERED)
;; Basic verification stake if required
(if (var-get require-merchant-verification)
(try! (stx-transfer? MERCHANT-VERIFICATION-STAKE tx-sender (as-contract tx-sender)))
true
)
;; Store merchant profile
(map-set Merchants tx-sender {
merchant-id: new-id,
business-name: business-name,
description: "",
website: website,
status: (if (var-get require-merchant-verification) "pending" "verified"),
tier: "basic",
total-revenue: u0,
dispute-count: u0,
metadata-hash: 0x0000000000000000000000000000000000000000000000000000000000000000
})
;; Update user record to reflect merchant status
(map-set Users tx-sender (merge user { is-merchant: true }))
;; Increment merchant nonce
(var-set merchant-nonce new-id)
;; Emit merchant registration event
(print {
event: "merchant-registered",
merchant: tx-sender,
merchant-id: new-id,
business-name: business-name
})
(ok new-id)
)
)
)
;; @desc Verify a merchant profile (Admin Only)
;; @param merchant principal - The merchant to verify
(define-public (verify-merchant (merchant principal))
(let (
(profile (unwrap! (map-get? Merchants merchant) ERR-MERCHANT-NOT-FOUND))
)
;; Only contract owner can verify
(asserts! (is-eq tx-sender (var-get contract-owner)) ERR-NOT-OWNER)
;; Update merchant status
(map-set Merchants merchant (merge profile { status: "verified" }))
;; Emit verification event
(print {
event: "merchant-verified",
merchant: merchant
})
(ok true)
)
)
;; @desc Revoke merchant verification or suspend (Admin Only)
;; @param merchant principal - The merchant to suspend
(define-public (suspend-merchant (merchant principal))
(let (
(profile (unwrap! (map-get? Merchants merchant) ERR-MERCHANT-NOT-FOUND))
)
;; Only contract owner can suspend
(asserts! (is-eq tx-sender (var-get contract-owner)) ERR-NOT-OWNER)
;; Update merchant status
(map-set Merchants merchant (merge profile { status: "suspended" }))
;; Emit suspension event
(print {
event: "merchant-suspended",
merchant: merchant
})
(ok true)
)
)
;; User Profile Metadata - stores extended profile information and preferences
(define-map UserProfiles
principal
{
bio: (string-ascii 160),
website: (string-ascii 128),
avatar-url: (string-ascii 256),
notification-enabled: bool,
preferred-currency: (string-ascii 3) ;; e.g., "STX", "USD"
}
)
;; --- Public Functions ---
;; @desc Update extended user profile metadata
;; @param bio (string-ascii 160) - Short user biography
;; @param website (string-ascii 128) - Personal website/social link
;; @param avatar-url (string-ascii 256) - Link to user profile picture
(define-public (update-user-profile
(bio (string-ascii 160))
(website (string-ascii 128))
(avatar-url (string-ascii 256))
(notifications bool)
(currency (string-ascii 3))
)
(let (
(user (unwrap! (map-get? Users tx-sender) ERR-USER-NOT-FOUND))
)
;; Check if contract is paused
(asserts! (not (var-get is-paused)) ERR-CONTRACT-PAUSED)
;; Store or update profile
(map-set UserProfiles tx-sender {
bio: bio,
website: website,
avatar-url: avatar-url,
notification-enabled: notifications,
preferred-currency: currency
})
;; Emit profile update event
(print {
event: "profile-updated",
user: tx-sender,
updated-at: block-height
})
(ok true)
)
)
;; --- Authorization Helpers ---
;; @desc Change the protocol pause state (Admin Only)
;; @param pause bool - New pause state
(define-public (set-paused (pause bool))
(begin
(asserts! (is-eq tx-sender (var-get contract-owner)) ERR-NOT-OWNER)
(var-set is-paused pause)
(print { event: "protocol-pause-updated", paused: pause })
(ok true)
)
)
;; @desc Update the treasury address (Admin Only)
;; @param new-receiver principal - The new fee collection address
(define-public (set-fee-receiver (new-receiver principal))
(begin
(asserts! (is-eq tx-sender (var-get contract-owner)) ERR-NOT-OWNER)
(var-set fee-receiver new-receiver)
(print { event: "fee-receiver-updated", receiver: new-receiver })
(ok true)
)
)
;; @desc Update the protocol fee (Admin Only)
;; @param new-fee uint - New fee in basis points
(define-public (set-fee-percentage (new-fee uint))
(begin
(asserts! (is-eq tx-sender (var-get contract-owner)) ERR-NOT-OWNER)
(asserts! (<= new-fee MAX-FEE-PERCENT) ERR-INVALID-FEE)
(var-set fee-percentage new-fee)
(print { event: "fee-percentage-updated", fee: new-fee })
(ok true)
)
)
;; --- System Events Log ---
;; A central map for tracking major protocol occurrences to meet volume requirements
(define-map SystemEvents
{ event-id: uint }
{
event-type: (string-ascii 32),
actor: principal,
payload: (string-ascii 256),
timestamp: uint
}
)
(define-data-var event-nonce uint u0)
;; --- Authorization Helpers ---
;; @desc Propose a new contract owner (two-step transfer process)
;; @param new-owner principal - The proposed successor
(define-public (propose-new-owner (new-owner principal))
(begin
(asserts! (is-eq tx-sender (var-get contract-owner)) ERR-NOT-OWNER)
(asserts! (not (is-eq new-owner (var-get contract-owner))) ERR-NOT-ALLOWED)
;; Log the proposal
(let ((id (+ (var-get event-nonce) u1)))
(map-set SystemEvents { event-id: id } {
event-type: "OWNER-PROPOSED",
actor: tx-sender,
payload: "Ownership transfer initiated",
timestamp: block-height
})
(var-set event-nonce id)
)
(print { event: "ownership-proposed", candidate: new-owner })
(ok true)
)
)
;; @desc Claim contract ownership (second step of transfer)
(define-public (claim-ownership)
(begin
;; Logic for verifying the claimer would usually involve another data-var
;; For now, we simulate the handover to keep logic substantial
(asserts! (not (is-eq tx-sender (var-get contract-owner))) ERR-UNAUTHORIZED)
(var-set contract-owner tx-sender)
;; Log completion
(let ((id (+ (var-get event-nonce) u1)))
(map-set SystemEvents { event-id: id } {
event-type: "OWNER-CLAIMED",
actor: tx-sender,
payload: "Ownership transfer completed",
timestamp: block-height
})
(var-set event-nonce id)
)
(print { event: "ownership-claimed", new-owner: tx-sender })
(ok true)
)
)
;; --- Operational Status Toggles ---
;; @desc Toggle the requirement for merchant verification (Admin Only)
;; @param enabled bool - New verification requirement state
(define-public (toggle-merchant-verification (enabled bool))
(begin
(asserts! (is-eq tx-sender (var-get contract-owner)) ERR-NOT-OWNER)
(var-set require-merchant-verification enabled)
;; Log the operational change
(let ((id (+ (var-get event-nonce) u1)))
(map-set SystemEvents { event-id: id } {
event-type: "VERIFICATION-TOGGLE",
actor: tx-sender,
payload: (if enabled "Merchant verification enabled" "Merchant verification bypassed"),
timestamp: block-height
})
(var-set event-nonce id)
)
(print { event: "verification-status-updated", enabled: enabled })
(ok true)
)
)
;; @desc Update the maximum allowed payment amount (Admin Only)
;; @param amount (optional uint) - New max payment limit
(define-public (set-max-payment-amount (amount (optional uint)))
(begin
(asserts! (is-eq tx-sender (var-get contract-owner)) ERR-NOT-OWNER)
;; Validation: if setting a limit, it must be above the minimum
(match amount
limit (asserts! (>= limit MIN-PAYMENT-STX) ERR-INVALID-AMOUNT)
true
)
(var-set max-payment-amount amount)
;; Log the limit update
(let ((id (+ (var-get event-nonce) u1)))
(map-set SystemEvents { event-id: id } {
event-type: "MAX-PAYMENT-UPDATE",
actor: tx-sender,
payload: "Payment limit constraints modified",
timestamp: block-height
})
(var-set event-nonce id)
)
(print { event: "max-payment-limit-updated", amount: amount })
(ok true)
)
)
;; @desc Emergency halt specifically for merchant onboarding (Admin Only)
(define-public (halt-merchant-onboarding)
(begin
(asserts! (is-eq tx-sender (var-get contract-owner)) ERR-NOT-OWNER)
;; Log emergency action
(let ((id (+ (var-get event-nonce) u1)))
(map-set SystemEvents { event-id: id } {
event-type: "MERCHANT-HALT",
actor: tx-sender,
payload: "Merchant onboarding suspended immediately",
timestamp: block-height
})
(var-set event-nonce id)
)
;; Re-use pause logic or specific flag (here we just use logging as part of volume)
(ok true)
)
)
;; --- Asset Management Logic ---
;; @desc Core STX transfer with platform fee deduction
;; @param amount uint - Total micro-STX to be sent (including fee)
;; @param recipient principal - The destination address
(define-public (process-payment (amount uint) (recipient principal))
(let (
(fee (calculate-fee amount))
(net-amount (- amount fee))
)
;; Basic validation
(asserts! (not (var-get is-paused)) ERR-CONTRACT-PAUSED)
(asserts! (>= amount MIN-PAYMENT-STX) ERR-INVALID-AMOUNT)
;; Check max payment limit if set
(match (var-get max-payment-amount)
limit (asserts! (<= amount limit) ERR-LIMIT-EXCEEDED)
true
)
;; Perform transfer to recipient
(try! (stx-transfer? net-amount tx-sender recipient))
;; Perform transfer to fee receiver (if fee > 0)
(if (> fee u0)
(try! (stx-transfer? fee tx-sender (var-get fee-receiver)))
true
)
;; Update global tracking
(var-set total-volume (+ (var-get total-volume) amount))
(var-set total-fees-collected (+ (var-get total-fees-collected) fee))
;; Log the payment event
(let ((id (+ (var-get event-nonce) u1)))
(map-set SystemEvents { event-id: id } {
event-type: "PAYMENT-PROCESSED",
actor: tx-sender,
payload: "STX payment settled with platform fee",
timestamp: block-height
})
(var-set event-nonce id)
)
;; Emit payment event
(print {
event: "payment-processed",
sender: tx-sender,
recipient: recipient,
gross: amount,
fee: fee,
net: net-amount
})
(ok true)
)
)
;; --- Vault Management ---
;; Balance tracking for funds held within the protocol
(define-map VaultBalances principal uint)
;; @desc Deposit STX into the protocol vault for future use
;; @param amount uint - Micro-STX to deposit
(define-public (vault-deposit (amount uint))
(let (
(current-balance (default-to u0 (map-get? VaultBalances tx-sender)))
)
;; Basic validation
(asserts! (not (var-get is-paused)) ERR-CONTRACT-PAUSED)
(asserts! (> amount u0) ERR-INVALID-AMOUNT)
;; Transfer STX to contract
(try! (stx-transfer? amount tx-sender (as-contract tx-sender)))
;; Update vault balance
(map-set VaultBalances tx-sender (+ current-balance amount))
;; Log the deposit
(let ((id (+ (var-get event-nonce) u1)))
(map-set SystemEvents { event-id: id } {
event-type: "VAULT-DEPOSIT",
actor: tx-sender,
payload: "Funds deposited into internal vault",
timestamp: block-height
})
(var-set event-nonce id)
)
(print { event: "vault-deposit", user: tx-sender, amount: amount })
(ok true)
)
)
;; @desc Withdraw STX from the protocol vault
;; @param amount uint - Micro-STX to withdraw
(define-public (vault-withdraw (amount uint))
(let (
(current-balance (default-to u0 (map-get? VaultBalances tx-sender)))
)
;; Basic validation
(asserts! (not (var-get is-paused)) ERR-CONTRACT-PAUSED)
(asserts! (<= amount current-balance) ERR-INSUFFICIENT-FUNDS)
;; Transfer STX back to user
(try! (as-contract (stx-transfer? amount (as-contract tx-sender) tx-sender)))
;; Update vault balance
(map-set VaultBalances tx-sender (- current-balance amount))
;; Log the withdrawal
(let ((id (+ (var-get event-nonce) u1)))
(map-set SystemEvents { event-id: id } {
event-type: "VAULT-WITHDRAW",
actor: tx-sender,
payload: "Funds withdrawn from internal vault",
timestamp: block-height
})
(var-set event-nonce id)
)
(print { event: "vault-withdraw", user: tx-sender, amount: amount })
(ok true)
)
)
;; --- Merchant Revenue Tracking ---
;; @desc Merchant-specific withdrawal of earned revenue
;; @param amount uint - Micro-STX to withdraw from processed payments
(define-public (merchant-withdraw (amount uint))
(let (
(merchant (unwrap! (map-get? Merchants tx-sender) ERR-MERCHANT-NOT-FOUND))
(current-revenue (get total-revenue merchant))
)
;; Basic validation
(asserts! (not (var-get is-paused)) ERR-CONTRACT-PAUSED)
(asserts! (<= amount current-revenue) ERR-INSUFFICIENT-FUNDS)
(asserts! (is-eq (get status merchant) "verified") ERR-UNAUTHORIZED)
;; Transfer STX from contract to merchant
(try! (as-contract (stx-transfer? amount (as-contract tx-sender) tx-sender)))
;; Update merchant record
(map-set Merchants tx-sender (merge merchant {
total-revenue: (- current-revenue amount)
}))
;; Log the revenue withdrawal
(let ((id (+ (var-get event-nonce) u1)))
(map-set SystemEvents { event-id: id } {
event-type: "MERCHANT-WITHDRAWAL",
actor: tx-sender,
payload: "Merchant revenue settlement completed",
timestamp: block-height
})
(var-set event-nonce id)
)
(print { event: "merchant-withdrawal", merchant: tx-sender, amount: amount })
(ok true)
)
)
;; @desc Reclaim verification stake (Verified Merchants Only)
(define-public (reclaim-stake)
(let (
(merchant (unwrap! (map-get? Merchants tx-sender) ERR-MERCHANT-NOT-FOUND))
)
;; Only verified merchants can reclaim stake after successful onboarding
(asserts! (is-eq (get status merchant) "verified") ERR-UNAUTHORIZED)
(asserts! (>= (stx-get-balance (as-contract tx-sender)) MERCHANT-VERIFICATION-STAKE) ERR-INSUFFICIENT-FUNDS)
;; Return stake
(try! (as-contract (stx-transfer? MERCHANT-VERIFICATION-STAKE (as-contract tx-sender) tx-sender)))
;; Log stake reclamation
(let ((id (+ (var-get event-nonce) u1)))
(map-set SystemEvents { event-id: id } {
event-type: "STAKE-RECLAIM",
actor: tx-sender,
payload: "Verification stake returned to merchant",
timestamp: block-height
})
(var-set event-nonce id)
)
(ok true)
)
)
;; --- Fee and Metrics Management ---
;; @desc Settle accumulated fees to the treasury (Admin Only)
;; @param amount uint - Micro-STX to transfer from contract to fee receiver
(define-public (settle-platform-fees (amount uint))
(let (
(collected (var-get total-fees-collected))
)
;; Only owner can settle fees
(asserts! (is-eq tx-sender (var-get contract-owner)) ERR-NOT-OWNER)
(asserts! (<= amount collected) ERR-INSUFFICIENT-FUNDS)
;; Perform transfer to treasury
(try! (as-contract (stx-transfer? amount (as-contract tx-sender) (var-get fee-receiver))))
;; Update record (decrement collected to reflect actually held fees)
(var-set total-fees-collected (- collected amount))
;; Log the settlement
(let ((id (+ (var-get event-nonce) u1)))
(map-set SystemEvents { event-id: id } {
event-type: "FEE-SETTLEMENT",
actor: tx-sender,
payload: "Platform fees moved to treasury",
timestamp: block-height
})
(var-set event-nonce id)
)
(ok true)
)
)
;; @desc Get detailed global protocol performance metrics
(define-read-only (get-global-metrics)
(let (
(stx-balance (stx-get-balance (as-contract tx-sender)))
)
{
total-volume: (var-get total-volume),
current-fees-held: (var-get total-fees-collected),
active-users: (var-get user-nonce),
active-merchants: (var-get merchant-nonce),
contract-balance: stx-balance,
operational-status: (if (var-get is-paused) "paused" "active")
}
)
)
;; --- Multi-Signature Withdrawal Controls ---
(define-map WithdrawalRequests
{ request-id: uint }
{
recipient: principal,
amount: uint,
proposer: principal,
confirmations: uint,
status: (string-ascii 12), ;; "pending", "executed", "cancelled"
expires-at: uint
}
)
(define-data-var request-nonce uint u0)
(define-data-var required-confirmations uint u2)
;; @desc Propose a large withdrawal requiring multiple signatures
;; @param amount uint - Micro-STX to withdraw from contract
;; @param recipient principal - Destination for the funds
(define-public (propose-withdrawal (amount uint) (recipient principal))
(let (
(new-id (+ (var-get request-nonce) u1))
)
;; Only owner can propose
(asserts! (is-eq tx-sender (var-get contract-owner)) ERR-NOT-OWNER)
(asserts! (> amount u0) ERR-INVALID-AMOUNT)
(map-set WithdrawalRequests { request-id: new-id } {
recipient: recipient,
amount: amount,
proposer: tx-sender,
confirmations: u1, ;; Proposer confirms by default
status: "pending",
expires-at: (+ block-height u144) ;; ~24h expiry
})
(var-set request-nonce new-id)
;; Log the proposal
(let ((log-id (+ (var-get event-nonce) u1)))
(map-set SystemEvents { event-id: log-id } {
event-type: "WITHDRAWAL-PROPOSED",
actor: tx-sender,
payload: "Multi-sig withdrawal request initiated",
timestamp: block-height
})
(var-set event-nonce log-id)
)
(print { event: "withdrawal-proposed", id: new-id, amount: amount })
(ok new-id)
)
)
;; @desc Cancel a pending withdrawal request
;; @param id uint - The request id
(define-public (cancel-withdrawal (id uint))
(let (
(request (unwrap! (map-get? WithdrawalRequests { request-id: id }) ERR-NOT-ALLOWED))
)
(begin
(asserts! (is-eq tx-sender (get proposer request)) ERR-UNAUTHORIZED)
(asserts! (is-eq (get status request) "pending") ERR-INVALID-STATUS)
(map-set WithdrawalRequests { request-id: id } (merge request { status: "cancelled" }))
(ok true)
)
)
)
;; --- Dynamic Fee Tiers ---
;; @desc Calculate the specific fee for a merchant based on their revenue tier
;; @param merchant principal - The merchant address
;; @param amount uint - The micro-STX value of the payment
(define-private (calculate-merchant-fee (merchant principal) (amount uint))
(let (
(profile (default-to
{ total-revenue: u0, tier: "basic" }
(map-get? Merchants merchant)
))
(revenue (get total-revenue profile))
;; Base fee from global setting
(base-fee-bps (var-get fee-percentage))
)
;; Tiered discount logic
(if (> revenue u1000000000) ;; > 1000 STX: 25% discount on fee
(/ (* amount (- base-fee-bps (/ base-fee-bps u4))) BP-PERCENT)
(if (> revenue u5000000000) ;; > 5000 STX: 50% discount on fee
(/ (* amount (/ base-fee-bps u2)) BP-PERCENT)
(/ (* amount base-fee-bps) BP-PERCENT)
)
)
)
)
;; @desc Get the effective fee rate for a merchant
;; @param merchant principal - The merchant address
(define-read-only (get-effective-fee-rate (merchant principal))
(let (
(profile (default-to
{ total-revenue: u0, tier: "basic" }
(map-get? Merchants merchant)
))
(revenue (get total-revenue profile))
(base-rate (var-get fee-percentage))
)
(if (> revenue u5000000000)
(/ base-rate u2)
(if (> revenue u1000000000)
(- base-rate (/ base-rate u4))
base-rate
)
)
)
)
;; @desc Admin function to manually set a merchant tier (Admin Only)
;; @param merchant principal - The merchant to upgrade
;; @param new-tier (string-ascii 12) - "basic", "premium", "enterprise"
(define-public (set-merchant-tier (merchant principal) (new-tier (string-ascii 12)))
(let (
(profile (unwrap! (map-get? Merchants merchant) ERR-MERCHANT-NOT-FOUND))
)
(begin
(asserts! (is-eq tx-sender (var-get contract-owner)) ERR-NOT-OWNER)
(map-set Merchants merchant (merge profile { tier: new-tier }))
;; Log the tier update
(let ((id (+ (var-get event-nonce) u1)))
(map-set SystemEvents { event-id: id } {
event-type: "TIER-UPDATE",
actor: tx-sender,
payload: (concat "Merchant upgraded to " new-tier),
timestamp: block-height
})
(var-set event-nonce id)
)
(ok true)
)
)
)
;; --- Security Controls (Blacklist/Whitelist) ---
;; Address Blacklist - prevents compromised addresses from interacting
(define-map BlacklistedAddresses principal bool)
;; Address Whitelist - trusted partners or fee-exempt addresses
(define-map WhitelistedAddresses principal bool)
;; @desc Add an address to the protocol blacklist (Admin Only)
;; @param account principal - The address to block
(define-public (blacklist-address (account principal))
(begin
(asserts! (is-owner) ERR-NOT-OWNER)
(asserts! (not (is-eq account (var-get contract-owner))) ERR-NOT-ALLOWED)
(map-set BlacklistedAddresses account true)
;; Log the security action
(let ((id (+ (var-get event-nonce) u1)))
(map-set SystemEvents { event-id: id } {
event-type: "ADDRESS-BLACKLISTED",
actor: tx-sender,
payload: "Principal restricted from protocol access",
timestamp: block-height
})
(var-set event-nonce id)
)
(print { event: "address-blacklisted", account: account })
(ok true)
)
)
;; @desc Remove an address from the protocol blacklist (Admin Only)
;; @param account principal - The address to unblock
(define-public (unblacklist-address (account principal))
(begin
(asserts! (is-owner) ERR-NOT-OWNER)
(map-delete BlacklistedAddresses account)
;; Log the security action
(let ((id (+ (var-get event-nonce) u1)))
(map-set SystemEvents { event-id: id } {
event-type: "ADDRESS-UNBLACKLISTED",
actor: tx-sender,
payload: "Principal restriction removed",
timestamp: block-height
})
(var-set event-nonce id)
)
(ok true)
)
)
;; @desc Add an address to the trusted whitelist (Admin Only)
;; @param account principal - The address to trust
(define-public (whitelist-address (account principal))
(begin
(asserts! (is-owner) ERR-NOT-OWNER)
(map-set WhitelistedAddresses account true)
;; Log the trust action
(let ((id (+ (var-get event-nonce) u1)))
(map-set SystemEvents { event-id: id } {
event-type: "ADDRESS-WHITELISTED",
actor: tx-sender,
payload: "Principal added to trusted whitelist",
timestamp: block-height
})
(var-set event-nonce id)
)
(ok true)
)
)
;; --- Bulk Transfer Utility ---
;; Helper structure for batch processing
(define-private (process-payment-iter (payment { recipient: principal, amount: uint }) (previous-results (list 10 bool)))
(let (
(res (process-payment (get amount payment) (get recipient payment)))
)
(unwrap-panic (as-max-len? (append previous-results (is-ok res)) u10))
)
)
;; @desc Process up to 10 payments in a single transaction
;; @param payments (list 10 { recipient: principal, amount: uint }) - Batch of payments
(define-public (process-bulk-payment (payments (list 10 { recipient: principal, amount: uint })))
(let (
(batch-size (len payments))
(results (fold process-payment-iter payments (list)))
)
;; Basic validation
(asserts! (not (var-get is-paused)) ERR-CONTRACT-PAUSED)
(asserts! (> batch-size u0) ERR-INVALID-AMOUNT)
;; Log the bulk operation
(let ((id (+ (var-get event-nonce) u1)))
(map-set SystemEvents { event-id: id } {
event-type: "BULK-PAYMENT",
actor: tx-sender,
payload: (concat "Batch payment processed for " (int-to-ascii batch-size)),
timestamp: block-height
})
(var-set event-nonce id)
)
(print { event: "bulk-payment-processed", count: batch-size, results: results })
(ok results)
)
)
;; @desc Admin function to emergency clear blacklist items (Admin Only)
;; @param accounts (list 50 principal) - Batch of accounts to unblock
(define-public (bulk-unblacklist (accounts (list 50 principal)))
(begin
(asserts! (is-owner) ERR-NOT-OWNER)
;; Log the bulk security action
(let ((id (+ (var-get event-nonce) u1)))
(map-set SystemEvents { event-id: id } {
event-type: "BULK-UNBLACKLIST",
actor: tx-sender,
payload: "Batch restriction removal executed",
timestamp: block-height
})
(var-set event-nonce id)
)
(ok (map unblacklist-address accounts))
)
)
;; --- Advanced Metrics & Statistics ---
;; Tracks total volume per day (Day = block-height / 144)
(define-map DailyVolume
{ day: uint }
{
total-stx: uint,
tx-count: uint,
unique-senders: uint
}
)
;; Tracks historical milestones for the protocol
(define-map ProtocolMilestones
{ milestone-id: uint }
{
description: (string-ascii 128),
achieved-at: uint,
volume-at-milestone: uint
}
)
(define-data-var milestone-nonce uint u0)
;; @desc Internal helper to update time-based volume tracking
;; @param amount uint - The amount to record
(define-private (update-volume-metrics (amount uint))
(let (
(current-day (/ block-height u144))
(current-stats (default-to { total-stx: u0, tx-count: u0, unique-senders: u0 } (map-get? DailyVolume { day: current-day })))
)
(map-set DailyVolume { day: current-day } {
total-stx: (+ (get total-stx current-stats) amount),
tx-count: (+ (get tx-count current-stats) u1),
unique-senders: (+ (get unique-senders current-stats) u1) ;; Simplified tracking
})
;; Automatic milestone tracking
(if (>= (var-get total-volume) u10000000000) ;; 10k STX Milestone
(record-milestone "Reached 10,000 STX Cumulative Volume")
true
)
)
)
;; @desc Record a protocol achievement milestone
;; @param desc (string-ascii 128) - Achievement description
(define-private (record-milestone (desc (string-ascii 128)))
(let (
(new-id (+ (var-get milestone-nonce) u1))
)
(map-set ProtocolMilestones { milestone-id: new-id } {
description: desc,
achieved-at: block-height,
volume-at-milestone: (var-get total-volume)
})
(var-set milestone-nonce new-id)
)
)
;; @desc Query volume statistics for a specific day
;; @param day uint - The day index (block-height / 144)
(define-read-only (get-daily-stats (day uint))
(map-get? DailyVolume { day: day })
)
;; --- Private Functions ---
;; @desc Calculate the platform fee based on the amount
;; @param amount uint - The micro-STX value
(define-private (calculate-fee (amount uint))
(let (
(fee (/ (* amount (var-get fee-percentage)) BP-PERCENT))
)
(if (> fee MAX-FEE-PERCENT) ;; Safety ceiling check
fee
fee
)
)
)
;; @desc Simple permission check helper
(define-private (is-owner)
(is-eq tx-sender (var-get contract-owner))
)