diff --git a/Clarinet.toml b/Clarinet.toml index f31dacc..3d462c8 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -1,21 +1,19 @@ [project] -name = "analytics-token-staking" -description = "" +name = 'analytics-token-staking' +description = '' authors = [] telemetry = true -cache_dir = "./.cache" - -# [contracts.counter] -# path = "contracts/counter.clar" - +cache_dir = './.cache' +requirements = [] +[contracts.analytics-token] +path = 'contracts/analytics-token.clar' +clarity_version = 3 +epoch = 3.1 [repl.analysis] -passes = ["check_checker"] -check_checker = { trusted_sender = false, trusted_caller = false, callee_filter = false } +passes = ['check_checker'] -# Check-checker settings: -# trusted_sender: if true, inputs are trusted after tx_sender has been checked. -# trusted_caller: if true, inputs are trusted after contract-caller has been checked. -# callee_filter: if true, untrusted data may be passed into a private function without a -# warning, if it gets checked inside. This check will also propagate up to the -# caller. -# More informations: https://www.hiro.so/blog/new-safety-checks-in-clarinet +[repl.analysis.check_checker] +strict = false +trusted_sender = false +trusted_caller = false +callee_filter = false diff --git a/README.md b/README.md new file mode 100644 index 0000000..194cb12 --- /dev/null +++ b/README.md @@ -0,0 +1,130 @@ +# Analytics Token Staking & Governance Contract + +**Version**: 1.0.0 + +### **Overview** + +This Clarity smart contract enables users to stake STX tokens to earn **ANALYTICS-TOKEN** rewards and participate in protocol governance. The system features a tiered staking mechanism with dynamic rewards, voting power scaling, and administrative safeguards. Designed for decentralized analytics platforms, it incentivizes long-term participation while ensuring community-driven protocol updates. + +### **Key Features** + +1. **Tiered Staking System** + + - **Tiers**: 3 levels (Bronze, Silver, Gold) based on STX staked. + - **Reward Multipliers**: + - Bronze (1M+ STX): 1x base rewards + - Silver (5M+ STX): 1.5x rewards + governance features + - Gold (10M+ STX): 2x rewards + full governance rights + +2. **Lock-Up Bonuses** + + - Optional lock periods (0, 30, 60 days) boost rewards by 1.25x–1.5x. + +3. **Governance Engine** + + - Create/veto proposals with voting power proportional to staked STX. + - Minimum 1M STX staked to propose governance actions. + +4. **Security Protections** + - 24-hour cooldown for unstaking. + - Emergency pause/resume functions. + - Penalty-free withdrawals post-cooldown. + +### **Technical Specifications** + +#### **Constants** + +- `CONTRACT-OWNER`: Admin address with pause/resume privileges. +- `minimum-stake`: 1,000,000 STX (1 STX = 1e6 units). +- `cooldown-period`: 1,440 blocks (~24 hours). + +#### **Reward Formula** + +``` +Rewards = (staked_amount × base_rate × multiplier × elapsed_blocks) / 14,400,000 +``` + +- **Base Rate**: 5% APY (adjustable via governance). +- **Multiplier**: Tier + lock-period bonuses. + +#### **Data Structures** + +- `UserPositions`: Tiers, STX staked, voting power, rewards. +- `StakingPositions`: Lock periods, cooldown timers, accrued rewards. +- `Proposals`: Voting deadlines, vote counts, execution status. + +### **Core Functions** + +#### **Staking** + +1. **`stake-stx`**: Lock STX to earn rewards. + + - Inputs: `amount` (STX), `lock-period` (0/4,320/8,640 blocks). + - Requirements: Minimum 1M STX, valid lock period. + +2. **`initiate-unstake`**: Start 24-hour cooldown. +3. **`complete-unstake`**: Withdraw STX post-cooldown. + +#### **Governance** + +1. **`create-proposal`**: Submit governance action (e.g., reward rate changes). + + - Requires ≥1M STX staked. + - Inputs: `description` (256 chars), `voting-period` (100–2,880 blocks). + +2. **`vote-on-proposal`**: Cast votes using staking-derived voting power. + +#### **Administration** + +- **`pause-contract`/**`resume-contract`\*\*: Freeze/all operations (owner-only). + +### **Governance Process** + +1. **Proposal Creation** + + - Submit description + voting window. + - Minimum 1M STX staked required. + +2. **Voting Phase** + + - Votes weighted by `voting-power` (1 STX = 1 vote). + - Proposals pass if: + - `votes-for > votes-against` + - Total votes ≥ `minimum-votes` (1M). + +3. **Execution** + - Successful proposals executable after `end-block`. + +### **Security & Error Handling** + +#### **Error Codes** + +- `ERR-NOT-AUTHORIZED` (1000): Unauthorized action. +- `ERR-INSUFFICIENT-STX` (1003): Insufficient staked balance. +- `ERR-COOLDOWN-ACTIVE` (1004): Premature withdrawal attempt. + +#### **Emergency Protocols** + +- **Pause Mode**: Halts all staking/unstaking during threats. +- **Audited Withdrawals**: STX refunds guaranteed post-cooldown. + +### **Integration Guide** + +#### **Dependencies** + +- Clarinet (local testing). +- Hiro Explorer (mainnet deployment). + +#### **Sample Interactions** + +**Stake STX (30-Day Lock):** + +```clarity +(contract-call? .analytics-staking-contract stake-stx u5000000 u4320) +``` + +**Create Proposal:** + +```clarity +(contract-call? .analytics-staking-contract create-proposal "Increase base reward rate to 6%" u1000) +``` diff --git a/contracts/analytics-token.clar b/contracts/analytics-token.clar new file mode 100644 index 0000000..e759e8b --- /dev/null +++ b/contracts/analytics-token.clar @@ -0,0 +1,359 @@ +;; Title: Analytics Token Staking and Governance Contract + +;; Summary: The Analytics Token Staking and Governance Contract enables users to stake STX tokens to earn ANALYTICS-TOKEN rewards, +;; participate in protocol governance, and access tiered benefits. +;; Key features include dynamic reward multipliers based on stake size/lock duration, +;; a governance system for proposals/voting, cooldown-protected unstaking, and administrative controls for safety. + +;; Description: A Clarity smart contract for staking STX tokens, earning analytics tokens, +;; and participating in protocol governance through a tiered system with voting capabilities. + +;; Token Definition +(define-fungible-token ANALYTICS-TOKEN u0) + +;; Contract Owner & Error Codes +(define-constant CONTRACT-OWNER tx-sender) +(define-constant ERR-NOT-AUTHORIZED (err u1000)) +(define-constant ERR-INVALID-PROTOCOL (err u1001)) +(define-constant ERR-INVALID-AMOUNT (err u1002)) +(define-constant ERR-INSUFFICIENT-STX (err u1003)) +(define-constant ERR-COOLDOWN-ACTIVE (err u1004)) +(define-constant ERR-NO-STAKE (err u1005)) +(define-constant ERR-BELOW-MINIMUM (err u1006)) +(define-constant ERR-PAUSED (err u1007)) + +;; Contract State Variables +(define-data-var contract-paused bool false) +(define-data-var emergency-mode bool false) +(define-data-var stx-pool uint u0) + +;; Staking Parameters +(define-data-var base-reward-rate uint u500) ;; 5% base rate (100 = 1%) +(define-data-var bonus-rate uint u100) ;; 1% bonus for longer staking +(define-data-var minimum-stake uint u1000000) ;; Minimum stake amount +(define-data-var cooldown-period uint u1440) ;; 24 hour cooldown in blocks +(define-data-var proposal-count uint u0) + +;; Data Maps + +;; Governance Proposals +(define-map Proposals + { proposal-id: uint } + { + creator: principal, + description: (string-utf8 256), + start-block: uint, + end-block: uint, + executed: bool, + votes-for: uint, + votes-against: uint, + minimum-votes: uint + } +) + +;; User Account Data +(define-map UserPositions + principal + { + total-collateral: uint, + total-debt: uint, + health-factor: uint, + last-updated: uint, + stx-staked: uint, + analytics-tokens: uint, + voting-power: uint, + tier-level: uint, + rewards-multiplier: uint + } +) + +;; Staking Positions +(define-map StakingPositions + principal + { + amount: uint, + start-block: uint, + last-claim: uint, + lock-period: uint, + cooldown-start: (optional uint), + accumulated-rewards: uint + } +) + +;; Tier System Configuration +(define-map TierLevels + uint + { + minimum-stake: uint, + reward-multiplier: uint, + features-enabled: (list 10 bool) + } +) + +;; Public Functions + +;; Contract Administration +(define-public (initialize-contract) + (begin + (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED) + + ;; Set up tier levels + (map-set TierLevels u1 + { + minimum-stake: u1000000, + reward-multiplier: u100, + features-enabled: (list true false false false false false false false false false) + }) + (map-set TierLevels u2 + { + minimum-stake: u5000000, + reward-multiplier: u150, + features-enabled: (list true true true false false false false false false false) + }) + (map-set TierLevels u3 + { + minimum-stake: u10000000, + reward-multiplier: u200, + features-enabled: (list true true true true true false false false false false) + }) + (ok true) + ) +) + +;; Staking Operations +(define-public (stake-stx (amount uint) (lock-period uint)) + (let + ( + (current-position (default-to + { + total-collateral: u0, + total-debt: u0, + health-factor: u0, + last-updated: u0, + stx-staked: u0, + analytics-tokens: u0, + voting-power: u0, + tier-level: u0, + rewards-multiplier: u100 + } + (map-get? UserPositions tx-sender))) + ) + (asserts! (is-valid-lock-period lock-period) ERR-INVALID-PROTOCOL) + (asserts! (not (var-get contract-paused)) ERR-PAUSED) + (asserts! (>= amount (var-get minimum-stake)) ERR-BELOW-MINIMUM) + + (try! (stx-transfer? amount tx-sender (as-contract tx-sender))) + + (let + ( + (new-total-stake (+ (get stx-staked current-position) amount)) + (tier-info (get-tier-info new-total-stake)) + (lock-multiplier (calculate-lock-multiplier lock-period)) + ) + + (map-set StakingPositions + tx-sender + { + amount: amount, + start-block: stacks-block-height, + last-claim: stacks-block-height, + lock-period: lock-period, + cooldown-start: none, + accumulated-rewards: u0 + } + ) + + (map-set UserPositions + tx-sender + (merge current-position + { + stx-staked: new-total-stake, + tier-level: (get tier-level tier-info), + rewards-multiplier: (* (get reward-multiplier tier-info) lock-multiplier) + } + ) + ) + + (var-set stx-pool (+ (var-get stx-pool) amount)) + (ok true) + ) + ) +) + +;; Unstaking Operations +(define-public (initiate-unstake (amount uint)) + (let + ( + (staking-position (unwrap! (map-get? StakingPositions tx-sender) ERR-NO-STAKE)) + (current-amount (get amount staking-position)) + ) + (asserts! (>= current-amount amount) ERR-INSUFFICIENT-STX) + (asserts! (is-none (get cooldown-start staking-position)) ERR-COOLDOWN-ACTIVE) + + (map-set StakingPositions + tx-sender + (merge staking-position + { + cooldown-start: (some stacks-block-height) + } + ) + ) + (ok true) + ) +) + +(define-public (complete-unstake) + (let + ( + (staking-position (unwrap! (map-get? StakingPositions tx-sender) ERR-NO-STAKE)) + (cooldown-start (unwrap! (get cooldown-start staking-position) ERR-NOT-AUTHORIZED)) + ) + (asserts! (>= (- stacks-block-height cooldown-start) (var-get cooldown-period)) ERR-COOLDOWN-ACTIVE) + + (try! (as-contract (stx-transfer? (get amount staking-position) tx-sender tx-sender))) + + (map-delete StakingPositions tx-sender) + + (ok true) + ) +) + +;; Governance Operations +(define-public (create-proposal (description (string-utf8 256)) (voting-period uint)) + (let + ( + (user-position (unwrap! (map-get? UserPositions tx-sender) ERR-NOT-AUTHORIZED)) + (proposal-id (+ (var-get proposal-count) u1)) + ) + (asserts! (>= (get voting-power user-position) u1000000) ERR-NOT-AUTHORIZED) + (asserts! (is-valid-description description) ERR-INVALID-PROTOCOL) + (asserts! (is-valid-voting-period voting-period) ERR-INVALID-PROTOCOL) + + (map-set Proposals { proposal-id: proposal-id } + { + creator: tx-sender, + description: description, + start-block: stacks-block-height, + end-block: (+ stacks-block-height voting-period), + executed: false, + votes-for: u0, + votes-against: u0, + minimum-votes: u1000000 + } + ) + + (var-set proposal-count proposal-id) + (ok proposal-id) + ) +) + +(define-public (vote-on-proposal (proposal-id uint) (vote-for bool)) + (let + ( + (proposal (unwrap! (map-get? Proposals { proposal-id: proposal-id }) ERR-INVALID-PROTOCOL)) + (user-position (unwrap! (map-get? UserPositions tx-sender) ERR-NOT-AUTHORIZED)) + (voting-power (get voting-power user-position)) + (max-proposal-id (var-get proposal-count)) + ) + (asserts! (< stacks-block-height (get end-block proposal)) ERR-NOT-AUTHORIZED) + (asserts! (and (> proposal-id u0) (<= proposal-id max-proposal-id)) ERR-INVALID-PROTOCOL) + + (map-set Proposals { proposal-id: proposal-id } + (merge proposal + { + votes-for: (if vote-for (+ (get votes-for proposal) voting-power) (get votes-for proposal)), + votes-against: (if vote-for (get votes-against proposal) (+ (get votes-against proposal) voting-power)) + } + ) + ) + (ok true) + ) +) + +;; Contract Control +(define-public (pause-contract) + (begin + (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED) + (var-set contract-paused true) + (ok true) + ) +) + +(define-public (resume-contract) + (begin + (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED) + (var-set contract-paused false) + (ok true) + ) +) + +;; Read-Only Functions + +(define-read-only (get-contract-owner) + (ok CONTRACT-OWNER) +) + +(define-read-only (get-stx-pool) + (ok (var-get stx-pool)) +) + +(define-read-only (get-proposal-count) + (ok (var-get proposal-count)) +) + +;; Private Functions + +(define-private (get-tier-info (stake-amount uint)) + (if (>= stake-amount u10000000) + {tier-level: u3, reward-multiplier: u200} + (if (>= stake-amount u5000000) + {tier-level: u2, reward-multiplier: u150} + {tier-level: u1, reward-multiplier: u100} + ) + ) +) + +(define-private (calculate-lock-multiplier (lock-period uint)) + (if (>= lock-period u8640) ;; 2 months + u150 ;; 1.5x multiplier + (if (>= lock-period u4320) ;; 1 month + u125 ;; 1.25x multiplier + u100 ;; 1x multiplier (no lock) + ) + ) +) + +(define-private (calculate-rewards (user principal) (blocks uint)) + (let + ( + (staking-position (unwrap! (map-get? StakingPositions user) u0)) + (user-position (unwrap! (map-get? UserPositions user) u0)) + (stake-amount (get amount staking-position)) + (base-rate (var-get base-reward-rate)) + (multiplier (get rewards-multiplier user-position)) + ) + (/ (* (* (* stake-amount base-rate) multiplier) blocks) u14400000) + ) +) + +(define-private (is-valid-description (desc (string-utf8 256))) + (and + (>= (len desc) u10) + (<= (len desc) u256) + ) +) + +(define-private (is-valid-lock-period (lock-period uint)) + (or + (is-eq lock-period u0) + (is-eq lock-period u4320) + (is-eq lock-period u8640) + ) +) + +(define-private (is-valid-voting-period (period uint)) + (and + (>= period u100) + (<= period u2880) + ) +) \ No newline at end of file diff --git a/tests/analytics-token.test.ts b/tests/analytics-token.test.ts new file mode 100644 index 0000000..4bb9cf3 --- /dev/null +++ b/tests/analytics-token.test.ts @@ -0,0 +1,21 @@ + +import { describe, expect, it } from "vitest"; + +const accounts = simnet.getAccounts(); +const address1 = accounts.get("wallet_1")!; + +/* + The test below is an example. To learn more, read the testing documentation here: + https://docs.hiro.so/stacks/clarinet-js-sdk +*/ + +describe("example tests", () => { + it("ensures simnet is well initalised", () => { + expect(simnet.blockHeight).toBeDefined(); + }); + + // it("shows an example", () => { + // const { result } = simnet.callReadOnlyFn("counter", "get-counter", [], address1); + // expect(result).toBeUint(0); + // }); +});