Source Code

;; ===================================
;; YIELD DUAL STACKING CONTRACT
;; ===================================
;; This contract manages yield distribution for participants who hold sBTC
;; and optionally stack STX. It operates in cycles with periodic snapshots
;; to calculate rewards based on holdings and stacking participation.

;; ===================================
;; ERROR CONSTANTS
;; ===================================
;; Organized in logical ranges for better maintainability:
;; 100-119: Enrollment & Participation
;; 120-139: Cycle Management
;; 140-159: Snapshot Operations
;; 160-179: Ratio & Weight Computation
;; 180-199: Reward Distribution
;; 200-219: Admin & Configuration
;; 220-239: Data Validation

;; Enrollment & Participation Errors (100-119)
(define-constant ERR_ACTIVATION_HEIGHT_NOT_MET (err u100))
(define-constant ERR_ALREADY_ENROLLED (err u101))
(define-constant ERR_USER_BLACKLISTED (err u102))
(define-constant ERR_NOT_ENROLLED (err u103))
(define-constant ERR_ENROLL_MINIMUM_HOLD_NOT_MET (err u104))
(define-constant ERR_CYCLE_IN_FUTURE (err u105))
(define-constant ERR_ALREADY_PROPOSED_RATIO (err u106))
(define-constant ERR_RATIO_NOT_PROPOSED (err u107))
(define-constant ERR_COMPUTE_RATIO_FOR_ALL_PARTICIPANTS_FIRST (err u108))
(define-constant ERR_COMPUTED_RATIO_ALL_PARTICIPANTS (err u109))
(define-constant ERR_RATIO_TOO_LOW (err u110))
(define-constant ERR_RATIO_TOO_HIGH (err u111))
(define-constant ERR_ALREADY_VALIDATED_RATIO (err u112))
(define-constant ERR_INVALID_RATIO_FOR_ZERO_STX (err u113))
(define-constant ERR_ALREADY_WHITELISTED (err u114))
(define-constant ERR_TRACKING_ADDRESS_NOT_WHITELISTED (err u115))
(define-constant ERR_NOT_BLACKLISTED (err u116))

;; Cycle Management Errors (120-139)
(define-constant ERR_NOT_NEW_CYCLE_YET (err u120))
(define-constant ERR_CYCLE_ENDED (err u121))
(define-constant ERR_CYCLE_ENDED_2 (err u122)) ;; This should never be thrown
(define-constant ERR_NOT_REWARDED_ALL (err u123))
(define-constant ERR_REWARDS_NOT_SENT_YET (err u124))

;; Snapshot Operation Errors (140-159)
(define-constant ERR_NOT_NEW_SNAPSHOT_YET (err u140))
(define-constant ERR_NOT_SNAPSHOTTED_ALL_PARTICIPANTS (err u141))
(define-constant ERR_NOT_ALL_SNAPSHOTS (err u142))
(define-constant ERR_SNAPSHOTS_NOT_CONCLUDED (err u143))
(define-constant ERR_SNAPSHOTS_ALREADY_CONCLUDED (err u144))
(define-constant ERR_STX_BLOCK_NOT_MATCHING (err u145))
(define-constant ERR_STX_BLOCK_IN_FUTURE (err u146))

;; Ratio & Weight Computation Errors (160-179)
(define-constant ERR_RATIOS_NOT_CONCLUDED (err u160))
(define-constant ERR_WEIGHTS_NOT_COMPUTED (err u161))
(define-constant ERR_WEIGHTS_ALREADY_COMPUTED (err u162))
(define-constant ERR_NOT_ALL_WEIGHTS_COMPUTED (err u163))

;; Reward Distribution Errors (180-199)
(define-constant ERR_CANNOT_DISTRIBUTE_REWARDS (err u180))
(define-constant ERR_ALREADY_REWARDED (err u181))
(define-constant ERR_SET_CAN_DISTRIBUTE_ALREADY_CALLED (err u182))
(define-constant ERR_ALREADY_FINALIZED (err u183))
(define-constant ERR_CANNOT_FINALIZE (err u184))

;; Admin & Configuration Errors (200-219)
(define-constant ERR_NOT_ADMIN (err u200))
(define-constant ERR_CONTRACT_ALREADY_ACTIVE (err u201))
(define-constant ERR_CONTRACT_NOT_ACTIVE (err u202))
(define-constant ERR_APR_TOO_LOW (err u210))
(define-constant ERR_APR_TOO_HIGH (err u211))
(define-constant ERR_MULTIPLIER_TOO_LOW (err u212))
(define-constant ERR_MULTIPLIER_TOO_HIGH (err u213))

;; Data Validation Errors (220-239)
(define-constant ERR_WRONG_CYCLE_ID (err u220))
(define-constant ERR_ALREADY_INSTANTIATED (err u221))

;; Emergency Functions Errors (240-259)
(define-constant ERR_INSUFFICIENT_FUNDS (err u240))

;; ===================================
;; CONFIGURATION CONSTANTS
;; ===================================

(define-constant SNAPSHOT_NOT_TAKEN_MARKER u1000)
(define-constant SCALING_FACTOR u100000000)
(define-constant MIN_APR_SCALED u1000000) ;; 1% APR - 0.01 * 10^8
(define-constant MAX_APR_SCALED u20000000) ;; 20% APR - 0.20 * 10^8

(define-constant RATIO_PRECISION_HELPER_VALUE u100000000) ;; 10^8 precision for ratios
(define-constant SQRT_RATIO_PRECISION_HELPER_VALUE u10000) ;; 10^4 precision for sqrt-ratios
(define-constant MIN_GOLDEN_RATIO u1) ;; Minimum allowed ratio (10^-8 in scaled terms) to prevent division by zero

;; ===================================
;; STATE VARIABLES - Contract Control
;; ===================================

(define-data-var is-contract-active bool false)
(define-data-var admin principal contract-caller)
;; Operation tracking: u0=snapshotting, u1=concluded, u2=proposed-ratio, u3=ratio-validated, u4=weights-finalized, u5=set-can-distribute, u6=finalized
(define-data-var last-operation-done uint u0)

;; ===================================
;; STATE VARIABLES - Cycle Management
;; ===================================

(define-data-var cycle-id uint u0)
(define-data-var current-cycle-stacks-block-height uint u0)
(define-data-var current-cycle-bitcoin-block-height uint u0)
(define-data-var current-cycle-total-sbtc uint u0)
(define-data-var current-cycle-total-stx uint u0)
(define-data-var current-cycle-participants-count uint u0)

;; Cycle configuration variables
(define-data-var blocks-per-snapshot uint u0)
(define-data-var snapshots-per-cycle uint u0) ;; last snapshot is: next-cycle-bitcoin-block - blocks-per-snapshot
(define-data-var next-blocks-per-snapshot uint (var-get blocks-per-snapshot))
(define-data-var next-snapshots-per-cycle uint (var-get snapshots-per-cycle))
(define-data-var next-cycle-bitcoin-block-height uint (+ (var-get current-cycle-bitcoin-block-height)
  (* (var-get blocks-per-snapshot) (var-get snapshots-per-cycle))
))
(define-data-var bitcoin-blocks-per-year uint u52500)

;; ===================================
;; STATE VARIABLES - Snapshot Management
;; ===================================

(define-data-var current-snapshot-bitcoin-block-height uint u0)
(define-data-var current-snapshot-stacks-block-height uint u0)
(define-data-var current-snapshot-total-sbtc uint u0)
(define-data-var current-snapshot-total-stx uint u0)
(define-data-var current-snapshot-count uint u0)
(define-data-var current-snapshot-index uint u0)
(define-data-var are-snapshots-finalized bool false)
(define-data-var is-liquid-stacking-enabled bool true)

;; Local variable for snapshot processing
(define-data-var snapshot-block-hash (buff 32) 0x0000000000000000000000000000000000000000000000000000000000000000)

;; ===================================
;; STATE VARIABLES - Rewards & Ratios
;; ===================================

(define-data-var is-ratio-validated bool false)
(define-data-var is-distribution-enabled bool false)
(define-data-var rewards-to-distribute uint u0)
(define-data-var participants-rewarded-count uint u0)

;; APR - annual percentage rate scaled by 10^8. This represents the maximum
;; percentage of the sBTC yield contributors are ready to give away as rewards
;; in a year.
(define-data-var APR uint u5000000)

;; Yield boost multiplier for dual stacking (M, where max boost = M+1)
;; Default is 9, meaning 10x max boost (M+1 = 10)
(define-data-var yield-boost-multiplier uint u9)

;; ===================================
;; STATE VARIABLES - Weight Computation
;; ===================================

(define-data-var total-weights-sum uint u0)
(define-data-var weights-computed-count uint u0)
(define-data-var are-weights-computed bool false)

;; ===================================
;; STATE VARIABLES - Enrollment
;; ===================================

(define-data-var next-cycle-participants-count uint u0)
(define-data-var min-sbtc-hold-required-for-enrollment uint u10000)

;; ===================================
;; STATE VARIABLES - Ratio Computation
;; ===================================

(define-data-var processing-ratio uint u0)
(define-data-var max-percentage-above-ratio uint u500) ;; 5%
(define-constant PRECISION_PERCENTAGE_RATIO u100)

;; ===================================
;; NETWORK-SPECIFIC INITIALIZATION
;; ===================================

;; Network-specific initialization
(var-set current-cycle-bitcoin-block-height u922150)
(var-set snapshots-per-cycle u14)
(var-set blocks-per-snapshot u150)

;; ===================================
;; DATA MAPS - Participant Management
;; ===================================

;; When someone enrolls, gets added to the map
;; When someone leaves, gets removed from the map
;; Use cycle relevant at get-stacks-block-info state of map when doing calculations
;; For DeFis the rewarded addresses will be the addresses of their reward SC
(define-map participants
  { address: principal }
  {
    stacking-address: principal, ;; same address as enrollment for users
    rewarded-address: principal, ;; default contract-caller if not set
    tracking-address: principal, ;; custom only for DeFis
  }
)

(define-map whitelisted-defi-tracking-addresses
  { address: principal } ;; their rewards SC
  { whitelisted: bool }
)

(define-map blacklist
  { address: principal }
  { blacklisted: bool }
)
;; (map-set blacklist {address: ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP} {blacklisted: true}) ;; wallet 8
;; the Foundation and other sBTC holders, will not be taken into account by default as they haven't enrolled in the SC (and they should not enroll)

;; ===================================
;; DATA MAPS - Holdings Tracking
;; ===================================

;; cycle should start when the PoX prepare phase starts
;; it should end when the next PoX cycle prepare phase starts

;; Enrolled address tracking per cycle
(define-map participant-holding
  {
    cycle-id: uint,
    address: principal,
  }
  {
    amount: uint,
    stacked: uint,
    last-snapshot: uint,
    rewarded: bool,
    reward-amount: uint,
  }
)

(define-map participant-addresses
  {
    cycle-id: uint,
    address: principal,
  }
  {
    stacking-address: principal, ;; same address as enrollment for users
    rewarded-address: principal, ;; default contract-caller if not set
    tracking-address: principal, ;; custom only for DeFis
  }
)

;; Track address for whole sBTC amount per cycle
(define-map tracking-holding
  {
    cycle-id: uint,
    tracking-address: principal,
  }
  {
    amount: uint,
    rewarded: bool,
  }
)

;; Stacking address for STX
(define-map stacking-holding
  {
    cycle-id: uint,
    stacking-address: principal,
  }
  { amount: uint }
)

;; Stacking address for STX
(define-map stacking-holding-snapshot-index
  {
    cycle-id: uint,
    snapshot-index: uint,
    stacking-address: principal,
  }
  { computed: bool }
)

;; Weight computation tracking
(define-map participant-weights
  {
    cycle-id: uint,
    tracking-address: principal,
  }
  { weight: uint }
)

(define-map participant-weight-tracked
  {
    cycle-id: uint,
    enrolled-address: principal,
  }
  { computed: bool }
)

;; ===================================
;; DATA MAPS - Rewards Management
;; ===================================

(define-map rewards-holding
  {
    cycle-id: uint,
    rewarded-address: principal,
  }
  { amount: uint }
)

;; ===================================
;; DATA MAPS - Cycle & Snapshot Data
;; ===================================

(define-map cycle-snapshot-to-stx-block-height
  {
    cycle-id: uint,
    snapshot-id: uint,
  }
  {
    stx-block-height: uint,
    bitcoin-block-height-stored: uint,
  }
)

(define-map distribution-finalized-stx-block-height-when-called
  { cycle-id: uint }
  { stx-block-height: uint }
)

(define-map yield-cycles-data
  { cycle-id: uint }
  {
    blocks-per-snapshot: uint,
    snapshots-per-cycle: uint,
    start-btc-block-height: uint,
    start-stx-block-height: uint,
  }
)

;; ===================================
;; DATA MAPS - Ratio Management
;; ===================================

(define-map proposed-ratio
  {
    cycle-id: uint,
    user: principal,
  }
  {
    golden-ratio: uint,
    participants-counted: uint,
    sbtc-below: uint,
    sbtc-above: uint,
    sbtc-equal: uint,
  }
)

(define-map track-proposed-given-ratio
  {
    cycle-id: uint,
    user: principal,
    ratio: uint,
  }
  { proposed: bool }
)

(define-map ratio-used
  { cycle-id: uint }
  { used-ratio: uint }
)

(define-map amount-below-ratio-cycle-user
  {
    cycle-id: uint,
    user: principal,
    ratio: uint,
  }
  { sbtc-amount: uint }
)

(define-map amount-above-ratio-cycle-user
  {
    cycle-id: uint,
    user: principal,
    ratio: uint,
  }
  { sbtc-amount: uint }
)

(define-map track-participant-computed-proposed-ratio
  {
    cycle-id: uint,
    user: principal,
    ratio: uint,
    user-address: principal,
  }
  { computed: bool }
)

(define-map track-participant-tracking-computed-proposed-ratio
  {
    cycle-id: uint,
    user: principal,
    ratio: uint,
    user-tracking-address: principal,
  }
  { computed: bool }
)

;; ===================================
;; INITIALIZATION FUNCTIONS
;; ===================================

;; Update the initial Bitcoin block height before contract activation
(define-public (update-initialize-block (new-bitcoin-block-height uint))
  (begin
    (asserts! (not (var-get is-contract-active))
      ERR_CONTRACT_ALREADY_ACTIVE
    )
    (asserts! (is-eq contract-caller (var-get admin)) ERR_NOT_ADMIN)
    (ok (var-set current-cycle-bitcoin-block-height
      new-bitcoin-block-height
    ))
  )
)

;; Contract initialization - activates the contract and starts the first cycle
(define-public (initialize-contract (stx-block-height uint))
  (begin
    (asserts!
      (>= burn-block-height
        (var-get current-cycle-bitcoin-block-height)
      )
      ERR_ACTIVATION_HEIGHT_NOT_MET
    )
    (asserts! (not (var-get is-contract-active))
      ERR_CONTRACT_ALREADY_ACTIVE
    )
    (asserts!
      (unwrap-panic (contract-call? .bitcoin-block-buffer
        validate-stx-block-brackets-btc-block stx-block-height
        (var-get current-cycle-bitcoin-block-height)
      ))
      ERR_STX_BLOCK_NOT_MATCHING
    )

    (var-set next-snapshots-per-cycle (var-get snapshots-per-cycle))
    (var-set next-blocks-per-snapshot (var-get blocks-per-snapshot))
    (reset-state-for-cycle stx-block-height)
    (var-set next-cycle-bitcoin-block-height
      (+ (var-get current-cycle-bitcoin-block-height)
        (* (var-get blocks-per-snapshot) (var-get snapshots-per-cycle))
      ))
    (update-snapshot-for-new-cycle stx-block-height)
    (map-set cycle-snapshot-to-stx-block-height {
      cycle-id: (var-get cycle-id),
      snapshot-id: (var-get current-snapshot-index),
    } {
      stx-block-height: stx-block-height,
      bitcoin-block-height-stored: (var-get current-cycle-bitcoin-block-height),
    })
    (ok (var-set is-contract-active true))
  )
)

;; Reset state variables for a new cycle
(define-private (reset-state-for-cycle (stx-block-height uint))
  (begin
    (var-set blocks-per-snapshot (var-get next-blocks-per-snapshot))
    (var-set snapshots-per-cycle (var-get next-snapshots-per-cycle))
    (var-set is-distribution-enabled false)
    (var-set current-cycle-stacks-block-height stx-block-height)
    (var-set current-cycle-total-sbtc u0)
    (var-set current-cycle-total-stx u0)
    (var-set current-snapshot-total-sbtc u0)
    (var-set current-snapshot-total-stx u0)
    (var-set participants-rewarded-count u0)
    (var-set are-snapshots-finalized false)
    (var-set is-ratio-validated false)
    (var-set are-weights-computed false)
    (var-set weights-computed-count u0)
    (var-set total-weights-sum u0)
    (map-set yield-cycles-data { cycle-id: (var-get cycle-id) } {
      blocks-per-snapshot: (var-get blocks-per-snapshot),
      snapshots-per-cycle: (var-get snapshots-per-cycle),
      start-btc-block-height: (var-get current-cycle-bitcoin-block-height),
      start-stx-block-height: stx-block-height,
    })
    (var-set last-operation-done u0) ;; snapshotting
    (var-set current-cycle-participants-count
      (at-block
        (unwrap-panic (get-stacks-block-info? id-header-hash stx-block-height))
        (var-get next-cycle-participants-count)
      ))
  )
)

;; Update snapshot variables for a new cycle
(define-private (update-snapshot-for-new-cycle (stx-block-height uint))
  (begin
    (var-set current-snapshot-bitcoin-block-height
      (var-get current-cycle-bitcoin-block-height)
    )
    (var-set current-snapshot-count u0)
    (var-set current-snapshot-index u0)
    (var-set current-snapshot-stacks-block-height stx-block-height)
  )
)

;; ===================================
;; ENROLLMENT FUNCTIONS
;; ===================================

;; Enroll a user in the yield program
(define-public (enroll (rewarded-address (optional principal)))
  (let ((rewards-recipient (default-to contract-caller rewarded-address)))
    (asserts!
      (is-none (map-get? participants { address: contract-caller }))
      ERR_ALREADY_ENROLLED
    )
    (asserts!
      (is-none (map-get? blacklist { address: contract-caller }))
      ERR_USER_BLACKLISTED
    )
    (asserts!
      (>=
        (unwrap-panic (contract-call?
          'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token
          get-balance-available contract-caller
        ))
        (var-get min-sbtc-hold-required-for-enrollment)
      )
      ERR_ENROLL_MINIMUM_HOLD_NOT_MET
    )
    (var-set next-cycle-participants-count
      (+ (var-get next-cycle-participants-count) u1)
    )
    (print {
      bitcoin-block-height: burn-block-height,
      reward-address: rewards-recipient,
      enrolled-address: contract-caller,
      stacking-address: contract-caller,
      tracking-address: contract-caller,
      cycle-id: (var-get cycle-id),
      function-name: "enroll",
    })
    (ok (map-set participants { address: contract-caller } {
      rewarded-address: rewards-recipient,
      stacking-address: contract-caller,
      tracking-address: contract-caller,
    }))
  )
)

;; Enroll a DeFi protocol with custom addresses (admin only)
(define-public (enroll-defi
    (defi-contract principal)
    (tracking-address principal)
    (rewarded-address principal)
    (stacking-address (optional principal))
  )
  (let ((stacking-recipient (default-to defi-contract stacking-address)))
    (asserts! (is-eq contract-caller (var-get admin)) ERR_NOT_ADMIN)
    (asserts!
      (is-none (map-get? participants { address: defi-contract }))
      ERR_ALREADY_ENROLLED
    )
    (asserts! (is-none (map-get? blacklist { address: defi-contract }))
      ERR_USER_BLACKLISTED
    )
    (var-set next-cycle-participants-count
      (+ (var-get next-cycle-participants-count) u1)
    )
    (print {
      bitcoin-block-height: burn-block-height,
      reward-address: rewarded-address,
      stacking-address: stacking-recipient,
      tracking-address: tracking-address,
      enrolled-address: defi-contract,
      cycle-id: (var-get cycle-id),
      function-name: "enroll",
    })
    (ok (map-set participants { address: defi-contract } {
      rewarded-address: rewarded-address,
      stacking-address: stacking-recipient,
      tracking-address: tracking-address,
    }))
  )
)

;; Batch enroll multiple DeFi protocols (admin only)
(define-public (enroll-defi-batch (defi-contracts (list
  900
  {
    defi-contract: principal,
    tracking-address: principal,
    rewarded-address: principal,
    stacking-address: (optional principal),
  }
)))
  (begin
    (asserts! (is-eq contract-caller (var-get admin)) ERR_NOT_ADMIN)
    (ok (map enroll-defi-one defi-contracts))
  )
)

;; Helper for batch DeFi enrollment
(define-private (enroll-defi-one (entry {
  defi-contract: principal,
  tracking-address: principal,
  rewarded-address: principal,
  stacking-address: (optional principal),
}))
  (let (
      (defi-contract (get defi-contract entry))
      (stacking-address (get stacking-address entry))
      (rewarded-address (get rewarded-address entry))
      (tracking-address (get tracking-address entry))
    )
    (try! (enroll-defi defi-contract tracking-address rewarded-address
      stacking-address
    ))
    (ok true)
  )
)

;; Opt out of the yield program
(define-public (opt-out)
  (begin
    (asserts!
      (is-some (map-get? participants { address: contract-caller }))
      ERR_NOT_ENROLLED
    )
    (ok (remove-participant contract-caller))
  )
)

;; Opt out a DeFi protocol (admin only)
(define-public (opt-out-defi (defi-contract principal))
  (begin
    (asserts! (is-eq contract-caller (var-get admin)) ERR_NOT_ADMIN)
    (asserts!
      (is-some (map-get? participants { address: defi-contract }))
      ERR_NOT_ENROLLED
    )
    (ok (remove-participant defi-contract))
  )
)

;; Batch opt out a DeFi protocol (admin only)
(define-public (opt-out-defi-batch (defi-contracts (list 200 principal)))
  (begin
    (asserts! (is-eq contract-caller (var-get admin)) ERR_NOT_ADMIN)
    (ok (map remove-participant defi-contracts))
  )
)

;; Add DeFi tracking address to whitelist - gives them maximum weight boost
(define-public (whitelist-defi-tracking (defi-rewards-contract principal))
  (begin
    (asserts! (is-eq contract-caller (var-get admin)) ERR_NOT_ADMIN)
    (asserts!
      (is-none (map-get? whitelisted-defi-tracking-addresses { address: defi-rewards-contract }))
      ERR_ALREADY_WHITELISTED
    )

    (print {
      bitcoin-block-height: burn-block-height,
      whitelisted-address: defi-rewards-contract,
      function-name: "whitelisted-defi-tracking-address",
    })
    (map-set whitelisted-defi-tracking-addresses { address: defi-rewards-contract } { whitelisted: true })
    (ok true)
  )
)

;; Remove DeFi tracking address from whitelist (admin only)
(define-public (remove-whitelisted-defi-tracking (defi-rewards-contract principal))
  (begin
    (asserts! (is-eq contract-caller (var-get admin)) ERR_NOT_ADMIN)
    (asserts!
      (is-some (map-get? whitelisted-defi-tracking-addresses { address: defi-rewards-contract }))
      ERR_TRACKING_ADDRESS_NOT_WHITELISTED
    )
    (print {
      bitcoin-block-height: burn-block-height,
      removed-whitelisted-address: defi-rewards-contract,
      function-name: "remove-whitelisted-defi-tracking-address",
    })
    (map-delete whitelisted-defi-tracking-addresses { address: defi-rewards-contract })
    (ok true)
  )
)

;; Batch remove DeFi tracking addresses from whitelist (admin only)
(define-public (remove-whitelisted-defi-tracking-batch (defi-rewards-contract (list 200 principal)))
  (begin
    (asserts! (is-eq contract-caller (var-get admin)) ERR_NOT_ADMIN)
    (ok (map remove-whitelisted-defi-tracking defi-rewards-contract))
  )
)

;; Remove a participant from enrollment
(define-private (remove-participant (address principal))
  (begin
    (print {
      bitcoin-block-height: burn-block-height,
      enrolled-address: address,
      cycle-id: (var-get cycle-id),
      function-name: "opt-out",
    })
    (map-delete participants { address: address })
    (var-set next-cycle-participants-count
      (- (var-get next-cycle-participants-count) u1)
    )
  )
)

(define-public (set-liquid-stacking (state bool))
  (begin
    (asserts! (is-eq contract-caller (var-get admin)) ERR_NOT_ADMIN)
    (ok (var-set is-liquid-stacking-enabled state))
  )
)

;; ===================================
;; PARTICIPANT MANAGEMENT FUNCTIONS
;; ===================================

;; Change reward address for a user
(define-public (change-reward-address (new-address principal))
  (let ((participant (map-get? participants { address: contract-caller })))
    (asserts! (is-some participant) ERR_NOT_ENROLLED)
    (print {
      bitcoin-block-height: burn-block-height,
      reward-address: new-address,
      stacking-address: (get stacking-address participant),
      tracking-address: (get tracking-address participant),
      enrolled-address: contract-caller,
      cycle-id: (var-get cycle-id),
      function-name: "update-participant-details",
    })
    (ok (map-set participants { address: contract-caller }
      (merge (unwrap-panic participant) { rewarded-address: new-address })
    ))
  )
)

;; Change reward address for a DeFi protocol (admin only)
(define-public (change-reward-address-defi
    (defi-contract principal)
    (new-reward-address principal)
  )
  (let ((participant (map-get? participants { address: defi-contract })))
    (asserts! (is-eq contract-caller (var-get admin)) ERR_NOT_ADMIN)
    (asserts! (is-some participant) ERR_NOT_ENROLLED)
    (print {
      bitcoin-block-height: burn-block-height,
      reward-address: new-reward-address,
      stacking-address: (get stacking-address participant),
      tracking-address: (get tracking-address participant),
      enrolled-address: defi-contract,
      cycle-id: (var-get cycle-id),
      function-name: "update-participant-details",
    })
    (ok (map-set participants { address: defi-contract }
      (merge (unwrap-panic participant) { rewarded-address: new-reward-address })
    ))
  )
)

;; Change stacking address for a DeFi protocol (admin only)
(define-public (change-stacking-address-defi
    (defi-contract principal)
    (new-stacking-address principal)
  )
  (let ((participant (map-get? participants { address: defi-contract })))
    (asserts! (is-eq contract-caller (var-get admin)) ERR_NOT_ADMIN)
    (asserts! (is-some participant) ERR_NOT_ENROLLED)
    (print {
      bitcoin-block-height: burn-block-height,
      reward-address: (get rewarded-address participant),
      stacking-address: new-stacking-address,
      tracking-address: (get tracking-address participant),
      enrolled-address: defi-contract,
      cycle-id: (var-get cycle-id),
      function-name: "update-participant-details",
    })
    (ok (map-set participants { address: defi-contract }
      (merge (unwrap-panic participant) { stacking-address: new-stacking-address })
    ))
  )
)

;; Change tracking address for a DeFi protocol (admin only)
(define-public (change-tracking-address-defi
    (defi-contract principal)
    (new-tracking-address principal)
  )
  (let ((participant (map-get? participants { address: defi-contract })))
    (asserts! (is-eq contract-caller (var-get admin)) ERR_NOT_ADMIN)
    (asserts! (is-some participant) ERR_NOT_ENROLLED)
    (print {
      bitcoin-block-height: burn-block-height,
      reward-address: (get rewarded-address participant),
      stacking-address: (get stacking-address participant),
      tracking-address: new-tracking-address,
      enrolled-address: defi-contract,
      cycle-id: (var-get cycle-id),
      function-name: "update-participant-details",
    })
    (ok (map-set participants { address: defi-contract }
      (merge (unwrap-panic participant) { tracking-address: new-tracking-address })
    ))
  )
)

;; Change addresses details for a DeFi protocol (admin only)
(define-public (change-addresses-defi
    (defi-contract principal)
    (new-tracking-address (optional principal))
    (new-reward-address (optional principal))
    (new-stacking-address (optional principal))
  )
  (let ((participant (map-get? participants { address: defi-contract })))
    (asserts! (is-eq contract-caller (var-get admin)) ERR_NOT_ADMIN)
    (asserts! (is-some participant) ERR_NOT_ENROLLED)
    (let (
        (local-reward-address (default-to (unwrap-panic (get rewarded-address participant))
          new-reward-address
        ))
        (local-stacking-address (default-to (unwrap-panic (get stacking-address participant))
          new-stacking-address
        ))
        (local-tracking-address (default-to (unwrap-panic (get tracking-address participant))
          new-tracking-address
        ))
      )
      (print {
        bitcoin-block-height: burn-block-height,
        reward-address: local-reward-address,
        stacking-address: local-stacking-address,
        tracking-address: local-tracking-address,
        enrolled-address: defi-contract,
        cycle-id: (var-get cycle-id),
        function-name: "update-participant-details",
      })
      (ok (map-set participants { address: defi-contract } {
        rewarded-address: local-reward-address,
        tracking-address: local-tracking-address,
        stacking-address: local-stacking-address,
      }))
    )
  )
)

;; Batch change addresses details multiple DeFi protocols (admin only)
(define-public (change-addresses-defi-batch (defi-contracts (list
  900
  {
    defi-contract: principal,
    tracking-address: (optional principal),
    rewarded-address: (optional principal),
    stacking-address: (optional principal),
  }
)))
  (begin
    (asserts! (is-eq contract-caller (var-get admin)) ERR_NOT_ADMIN)
    (ok (map change-addresses-defi-one defi-contracts))
  )
)

;; Helper for batch DeFi change addresses details
(define-private (change-addresses-defi-one (entry {
  defi-contract: principal,
  tracking-address: (optional principal),
  rewarded-address: (optional principal),
  stacking-address: (optional principal),
}))
  (let (
      (defi-contract (get defi-contract entry))
      (stacking-address (get stacking-address entry))
      (rewarded-address (get rewarded-address entry))
      (tracking-address (get tracking-address entry))
    )
    (try! (change-addresses-defi defi-contract tracking-address
      rewarded-address stacking-address
    ))
    (ok true)
  )
)

;; ===================================
;; BLACKLIST MANAGEMENT FUNCTIONS
;; ===================================

;; Add an address to the blacklist (admin only)
(define-public (add-blacklisted (address principal))
  (let ((participant (map-get? participants { address: address })))
    (asserts! (is-eq (var-get admin) contract-caller) ERR_NOT_ADMIN)
    (if (is-some participant)
      (begin
        (remove-participant address)
        (print {
          bitcoin-block-height: burn-block-height,
          reward-address: (get rewarded-address participant),
          stacking-address: (get stacking-address participant),
          tracking-address: (get tracking-address participant),
          enrolled-address: address,
          cycle-id: (var-get cycle-id),
          function-name: "blacklisted",
        })
        (ok (map-set blacklist { address: address } { blacklisted: true }))
      )
      (begin
        (print {
          bitcoin-block-height: burn-block-height,
          enrolled-address: address,
          cycle-id: (var-get cycle-id),
          function-name: "blacklisted",
        })
        (ok (map-set blacklist { address: address } { blacklisted: true }))
      )
    )
  )
)

;; Batch add addresses to blacklist (admin only)
(define-public (add-blacklisted-batch (addresses (list 200 principal)))
  (begin
    (asserts! (is-eq (var-get admin) contract-caller) ERR_NOT_ADMIN)
    (ok (map add-blacklisted addresses))
  )
)

;; Remove an address from the blacklist (admin only)
(define-public (remove-blacklisted (address principal))
  (begin
    (asserts! (is-eq (var-get admin) contract-caller) ERR_NOT_ADMIN)
    (asserts! (is-some (map-get? blacklist { address: address }))
      ERR_NOT_BLACKLISTED
    )
    (print {
      bitcoin-block-height: burn-block-height,
      enrolled-address: address,
      cycle-id: (var-get cycle-id),
      function-name: "unblacklisted",
    })
    (ok (map-delete blacklist { address: address }))
  )
)

;; Batch remove addresses from blacklist (admin only)
(define-public (remove-blacklisted-batch (addresses (list 200 principal)))
  (begin
    (asserts! (is-eq (var-get admin) contract-caller) ERR_NOT_ADMIN)
    (ok (map remove-blacklisted addresses))
  )
)

;; ===================================
;; ADMIN CONFIGURATION FUNCTIONS
;; ===================================

;; Update admin address
(define-public (update-admin (new-admin-address principal))
  (begin
    (asserts! (is-eq contract-caller (var-get admin)) ERR_NOT_ADMIN)
    (print {
      bitcoin-block-height: burn-block-height,
      admin-address: new-admin-address,
      cycle-id: (var-get cycle-id),
      function-name: "update-admin",
    })
    (ok (var-set admin new-admin-address))
  )
)

;; Update minimum hold requirement for enrollment
(define-public (update-min-sbtc-hold-required-for-enrollment (new-min-hold uint))
  (begin
    (asserts! (is-eq contract-caller (var-get admin)) ERR_NOT_ADMIN)
    (print {
      bitcoin-block-height: burn-block-height,
      min-sbtc-hold-required-for-enrollment: new-min-hold,
      cycle-id: (var-get cycle-id),
      function-name: "update-min-enrollment",
    })
    (ok (var-set min-sbtc-hold-required-for-enrollment new-min-hold))
  )
)

;; Update the number of blocks per snapshot (applies to next cycle)
(define-public (update-snapshot-length (updated-blocks-per-snapshot uint))
  (begin
    (asserts! (is-eq (var-get admin) contract-caller) ERR_NOT_ADMIN)
    (print {
      bitcoin-block-height: burn-block-height,
      snapshot-length: updated-blocks-per-snapshot,
      cycle-id: (var-get cycle-id),
      function-name: "update-snapshot-length",
    })
    (ok (var-set next-blocks-per-snapshot updated-blocks-per-snapshot))
  )
)

;; Update the number of snapshots per cycle (applies to next cycle)
(define-public (update-snapshots-per-cycle (updated-snapshots-per-cycle uint))
  (begin
    (asserts! (is-eq (var-get admin) contract-caller) ERR_NOT_ADMIN)
    (print {
      bitcoin-block-height: burn-block-height,
      snapshots-per-cycle: updated-snapshots-per-cycle,
      cycle-id: (var-get cycle-id),
      function-name: "update-snapshots-per-cycle",
    })
    (ok (var-set next-snapshots-per-cycle updated-snapshots-per-cycle))
  )
)

;; Update both snapshots per cycle and blocks per snapshot (applies to next cycle)
(define-public (update-cycle-data
    (updated-snapshots-per-cycle uint)
    (updated-blocks-per-snapshot uint)
  )
  (begin
    (asserts! (is-eq (var-get admin) contract-caller) ERR_NOT_ADMIN)
    (print {
      bitcoin-block-height: burn-block-height,
      snapshot-length: updated-blocks-per-snapshot,
      snapshots-per-cycle: updated-snapshots-per-cycle,
      cycle-id: (var-get cycle-id),
      function-name: "update-cycle-data",
    })
    (var-set next-snapshots-per-cycle updated-snapshots-per-cycle)
    (ok (var-set next-blocks-per-snapshot updated-blocks-per-snapshot))
  )
)

;; Update both snapshots per cycle and blocks per snapshot (applies to first cycle)
(define-public (update-cycle-data-before-initialized
    (updated-snapshots-per-cycle uint)
    (updated-blocks-per-snapshot uint)
  )
  (begin
    (asserts! (is-eq (var-get admin) contract-caller) ERR_NOT_ADMIN)

    (asserts! (not (var-get is-contract-active))
      ERR_CONTRACT_ALREADY_ACTIVE
    )
    (print {
      bitcoin-block-height: burn-block-height,
      snapshot-length: updated-blocks-per-snapshot,
      snapshots-per-cycle: updated-snapshots-per-cycle,
      cycle-id: (var-get cycle-id),
      function-name: "update-cycle-data",
    })
    (var-set snapshots-per-cycle updated-snapshots-per-cycle)
    (ok (var-set blocks-per-snapshot updated-blocks-per-snapshot))
  )
)

;; Update both snapshots per cycle and blocks per snapshot (applies to next cycle)
(define-public (update-bitcoin-blocks-per-year (updated-bitcoin-blocks-per-year uint))
  (begin
    (asserts! (is-eq (var-get admin) contract-caller) ERR_NOT_ADMIN)
    (print {
      bitcoin-block-height: burn-block-height,
      cycle-id: (var-get cycle-id),
      bitcoin-blocks-per-year: updated-bitcoin-blocks-per-year,
      function-name: "update-bitcoin-blocks-per-year",
    })
    (ok (var-set bitcoin-blocks-per-year updated-bitcoin-blocks-per-year))
  )
)

;; Update APR (Annual Percentage Rate, applies to next cycle)
(define-public (update-APR (new-apr-one-eight uint))
  (begin
    (asserts! (is-eq (var-get admin) contract-caller) ERR_NOT_ADMIN)
    (asserts! (>= new-apr-one-eight MIN_APR_SCALED) ERR_APR_TOO_LOW)
    (asserts! (<= new-apr-one-eight MAX_APR_SCALED) ERR_APR_TOO_HIGH)
    (ok (var-set APR new-apr-one-eight))
  )
)

;; Update yield boost multiplier for dual stacking (M, where max boost = M+1)
;; Default is 9, meaning 10x max boost (M+1 = 10)
;; Note: This takes effect immediately (unlike APR which applies to next cycle)
(define-public (update-yield-boost-multiplier (new-multiplier uint))
  (begin
    (asserts! (is-eq (var-get admin) contract-caller) ERR_NOT_ADMIN)
    (asserts! (>= new-multiplier u1) ERR_MULTIPLIER_TOO_LOW)
    (asserts! (<= new-multiplier u20) ERR_MULTIPLIER_TOO_HIGH) ;; capped at 21x max boost
    (print {
      bitcoin-block-height: burn-block-height,
      yield-boost-multiplier: new-multiplier,
      max-boost: (+ new-multiplier u1),
      cycle-id: (var-get cycle-id),
      function-name: "update-yield-boost-multiplier",
    })
    (ok (var-set yield-boost-multiplier new-multiplier))
  )
)

;; Emergency withdraw function for admin to withdraw sBTC funds
(define-public (emergency-withdraw-sbtc
    (amount uint)
    (recipient principal)
  )
  (begin
    (asserts! (is-eq contract-caller (var-get admin)) ERR_NOT_ADMIN)
    (asserts! (> amount u0) ERR_INSUFFICIENT_FUNDS)
    (let ((contract-balance (unwrap-panic (contract-call?
        'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token
        get-balance-available (as-contract contract-caller)
      ))))
      (asserts! (>= contract-balance amount) ERR_INSUFFICIENT_FUNDS)
      (print {
        bitcoin-block-height: burn-block-height,
        cycle-id: (var-get cycle-id),
        amount: amount,
        recipient: recipient,
        contract-balance: contract-balance,
        function-name: "emergency-withdraw-sbtc",
      })
      (try! (as-contract (contract-call?
        'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token
        transfer amount tx-sender recipient none
      )))
      (ok true)
    )
  )
)

;; Read-only function to check contract's sBTC balance
(define-read-only (get-contract-sbtc-balance)
  (unwrap-panic (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token
    get-balance-available (as-contract contract-caller)
  ))
)

;; ===================================
;; SNAPSHOT FUNCTIONS
;; ===================================

;; Capture snapshot balances for a list of participants
(define-public (capture-snapshot-balances (principals (list 900 principal)))
  (let ((stx-id-header-hash (unwrap!
      (get-stacks-block-info? id-header-hash
        (var-get current-snapshot-stacks-block-height)
      )
      ERR_STX_BLOCK_IN_FUTURE
    )))
    (asserts! (var-get is-contract-active) ERR_CONTRACT_NOT_ACTIVE)
    (var-set snapshot-block-hash stx-id-header-hash)
    (let ((snapshot-total (fold capture-participant-balances principals u0)))
      (var-set current-snapshot-total-sbtc
        (+ (var-get current-snapshot-total-sbtc) snapshot-total)
      )
      (ok snapshot-total)
    )
  )
)

;; Capture and update balances for a single user during snapshot
(define-private (capture-participant-balances
    (address principal)
    (current-sbtc-total uint)
  )
  (let (
      (cycle-id-current (var-get cycle-id))
      (is-user-enrolled-this-cycle (is-enrolled-this-cycle address))
      (participant-data (at-block
        (unwrap-panic (get-stacks-block-info? id-header-hash
          (var-get current-cycle-stacks-block-height)
        ))
        (map-get? participants { address: address })
      ))
      (participant-hold (map-get? participant-holding {
        cycle-id: cycle-id-current,
        address: address,
      }))
      (sbtc-balance (at-block
        ;; nonexistent sbtc-balance on that block height - never happens
        (var-get snapshot-block-hash)
        (unwrap-panic (contract-call?
          'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token
          get-balance-available address
        ))
      ))
      (local-current-snapshot-index (var-get current-snapshot-index))
    )
    ;; only add those that are part of the pool and aren't already snapshotted
    (if (or
        (is-none participant-data)
        (and (is-some participant-data) (is-eq
          (default-to SNAPSHOT_NOT_TAKEN_MARKER
            (get last-snapshot participant-hold)
          )
          local-current-snapshot-index
        ))
      )
      ;; don't count him
      current-sbtc-total
      (let (
          ;; the unwrap panic never throws as we have the other map checked, meaning we also have value here
          (stacked-address (unwrap-panic (get stacking-address participant-data)))
          (stacking-hold (map-get? stacking-holding {
            cycle-id: cycle-id-current,
            stacking-address: stacked-address,
          }))
          (new-total (+ current-sbtc-total sbtc-balance))
          ;; the unwrap panic never throws as we have the other map checked, meaning we also have value here
          (tracking-address (unwrap-panic (get tracking-address participant-data)))
          (tracking-hold (map-get? tracking-holding {
            cycle-id: cycle-id-current,
            tracking-address: tracking-address,
          }))
          (stx-stacked (if (get-is-whitelisted-defi tracking-address)
            u0
            (get-amount-stacked-at-block-height stacked-address
              (var-get current-snapshot-stacks-block-height)
            )
          ))
        )
        (var-set current-snapshot-count
          (+ (var-get current-snapshot-count) u1)
        )
        (var-set current-snapshot-total-stx
          (+ (var-get current-snapshot-total-stx) stx-stacked)
        )
        (map-set participant-holding {
          cycle-id: cycle-id-current,
          address: address,
        } {
          amount: (+ sbtc-balance (default-to u0 (get amount participant-hold))), ;; just for data, not used afterwards in computations
          stacked: (+ stx-stacked (default-to u0 (get stacked participant-hold))),
          last-snapshot: local-current-snapshot-index,
          rewarded: false,
          reward-amount: u0,
        })
        (map-set tracking-holding {
          cycle-id: cycle-id-current,
          tracking-address: tracking-address,
        } {
          amount: (+ sbtc-balance (default-to u0 (get amount tracking-hold))),
          rewarded: false,
        })
        (if (is-some (map-get? stacking-holding-snapshot-index {
            cycle-id: cycle-id-current,
            snapshot-index: local-current-snapshot-index,
            stacking-address: stacked-address,
          }))
          true
          (begin
            (map-set stacking-holding {
              cycle-id: cycle-id-current,
              stacking-address: stacked-address,
            } { amount: (+ stx-stacked (default-to u0 (get amount stacking-hold))) }
            )
            (map-set stacking-holding-snapshot-index {
              cycle-id: cycle-id-current,
              snapshot-index: local-current-snapshot-index,
              stacking-address: stacked-address,
            } { computed: true }
            )
          )
        )
        (print {
          cycle-id: cycle-id-current,
          snapshot-index: local-current-snapshot-index,
          sbtc-balance: sbtc-balance,
          stx-stacked: stx-stacked,
          tracking-address: tracking-address,
          stacking-address: stacked-address,
          enrolled-address: address,
          function-name: "compute-current-snapshot-balance",
        })
        new-total
      )
    )
  )
)

;; Advance to the next snapshot in the current cycle
(define-public (advance-to-next-snapshot (new-stx-block-height uint))
  (let ((next-snapshot-bitcoin-block-height (+ (var-get current-snapshot-bitcoin-block-height)
      (var-get blocks-per-snapshot)
    )))
    (asserts! (var-get is-contract-active) ERR_CONTRACT_NOT_ACTIVE)
    (asserts! (>= burn-block-height next-snapshot-bitcoin-block-height)
      ERR_NOT_NEW_SNAPSHOT_YET
    )
    (asserts!
      (is-eq (var-get current-snapshot-count)
        (var-get current-cycle-participants-count)
      )
      ERR_NOT_SNAPSHOTTED_ALL_PARTICIPANTS
    )
    (asserts!
      (not (is-eq (var-get snapshots-per-cycle)
        (+ (var-get current-snapshot-index) u1)
      ))
      ERR_CYCLE_ENDED
    )
    (asserts!
      (< next-snapshot-bitcoin-block-height
        (var-get next-cycle-bitcoin-block-height)
      )
      ERR_CYCLE_ENDED_2
    )
    (asserts!
      (unwrap-panic (contract-call? .bitcoin-block-buffer
        validate-stx-block-brackets-btc-block new-stx-block-height
        next-snapshot-bitcoin-block-height
      ))
      ERR_STX_BLOCK_NOT_MATCHING
    )

    (var-set current-snapshot-bitcoin-block-height
      next-snapshot-bitcoin-block-height
    )
    (var-set current-cycle-total-sbtc
      (+ (var-get current-cycle-total-sbtc)
        (var-get current-snapshot-total-sbtc)
      ))
    (var-set current-cycle-total-stx
      (+ (var-get current-cycle-total-stx)
        (var-get current-snapshot-total-stx)
      ))
    (var-set current-snapshot-total-sbtc u0)
    (var-set current-snapshot-total-stx u0)
    (var-set current-snapshot-count u0)
    (var-set current-snapshot-index
      (+ (var-get current-snapshot-index) u1)
    )
    (map-set cycle-snapshot-to-stx-block-height {
      cycle-id: (var-get cycle-id),
      snapshot-id: (var-get current-snapshot-index),
    } {
      stx-block-height: new-stx-block-height,
      bitcoin-block-height-stored: next-snapshot-bitcoin-block-height,
    })
    (ok (var-set current-snapshot-stacks-block-height new-stx-block-height))
  )
)

;; Finalize all snapshots for the current cycle
(define-public (finalize-snapshots)
  (begin
    (asserts! (var-get is-contract-active) ERR_CONTRACT_NOT_ACTIVE)
    (asserts! (not (var-get are-snapshots-finalized))
      ERR_SNAPSHOTS_ALREADY_CONCLUDED
    )
    (asserts!
      (is-eq (+ (var-get current-snapshot-index) u1)
        (var-get snapshots-per-cycle)
      )
      ERR_NOT_ALL_SNAPSHOTS
    )
    (asserts!
      (is-eq (var-get current-snapshot-count)
        (var-get current-cycle-participants-count)
      )
      ERR_NOT_SNAPSHOTTED_ALL_PARTICIPANTS
    )
    ;; Add in the calculation the total amount from the last snapshot
    (var-set current-cycle-total-sbtc
      (+ (var-get current-cycle-total-sbtc)
        (var-get current-snapshot-total-sbtc)
      ))
    (var-set current-cycle-total-stx
      (+ (var-get current-cycle-total-stx)
        (var-get current-snapshot-total-stx)
      ))
    (print {
      cycle-id: (var-get cycle-id),
      current-cycle-total-sbtc: (var-get current-cycle-total-sbtc),
      current-cycle-total-stx: (var-get current-cycle-total-stx),
      function-name: "conclude-cycle-snapshots",
    })
    (var-set last-operation-done u1) ;; concluded
    (ok (var-set are-snapshots-finalized true))
  )
)

;; ===================================
;; RATIO COMPUTATION FUNCTIONS
;; ===================================

;; Propose a golden ratio for the current cycle
(define-public (propose-golden-ratio (ratio uint))
  (begin
    (asserts! (var-get are-snapshots-finalized)
      ERR_SNAPSHOTS_NOT_CONCLUDED
    )
    (asserts!
      (is-none (map-get? ratio-used { cycle-id: (var-get cycle-id) }))
      ERR_ALREADY_VALIDATED_RATIO
    )
    (asserts!
      (is-none (map-get? proposed-ratio {
        cycle-id: (var-get cycle-id),
        user: tx-sender,
      }))
      ERR_ALREADY_PROPOSED_RATIO
    )
    (map-set proposed-ratio {
      cycle-id: (var-get cycle-id),
      user: tx-sender,
    } {
      golden-ratio: ratio,
      participants-counted: u0,
      sbtc-below: u0,
      sbtc-above: u0,
      sbtc-equal: u0,
    })
    (var-set last-operation-done u2) ;; proposed-ratio
    (ok true)
  )
)

;; Change a previously proposed golden ratio before validation
(define-public (change-proposed-golden-ratio (ratio uint))
  (begin
    (asserts!
      (is-some (map-get? proposed-ratio {
        cycle-id: (var-get cycle-id),
        user: tx-sender,
      }))
      ERR_RATIO_NOT_PROPOSED
    )
    (asserts!
      (is-none (map-get? ratio-used { cycle-id: (var-get cycle-id) }))
      ERR_ALREADY_VALIDATED_RATIO
    )
    (map-set proposed-ratio {
      cycle-id: (var-get cycle-id),
      user: tx-sender,
    } {
      golden-ratio: ratio,
      participants-counted: u0,
      sbtc-below: u0,
      sbtc-above: u0,
      sbtc-equal: u0,
    })
    (ok true)
  )
)

;; Tally participant ratios relative to proposed golden ratio
;; Accumulates sBTC amounts above/below the proposed ratio for validation
(define-public (tally-participant-ratios (principals (list 900 principal)))
  (let (
      (proposed-ratio-map (unwrap!
        (map-get? proposed-ratio {
          cycle-id: (var-get cycle-id),
          user: tx-sender,
        })
        ERR_RATIO_NOT_PROPOSED
      ))
      (golden-ratio (get golden-ratio proposed-ratio-map))
      (participants-count (get participants-counted proposed-ratio-map))
    )
    (asserts!
      (is-none (map-get? ratio-used { cycle-id: (var-get cycle-id) }))
      ERR_ALREADY_VALIDATED_RATIO
    )
    (asserts!
      (< participants-count (var-get current-cycle-participants-count))
      ERR_COMPUTED_RATIO_ALL_PARTICIPANTS
    )
    (var-set processing-ratio golden-ratio)
    (fold tally-user-ratio principals u0)
    (ok true)
  )
)

;; Helper function to tally user ratio relative to proposed golden ratio
(define-private (tally-user-ratio
    (address principal)
    (random-irrelevant uint)
  )
  (let (
      (current-cycle-id (var-get cycle-id))
      (participant-state (unwrap-panic (at-block
        (unwrap-panic (get-stacks-block-info? id-header-hash
          (var-get current-cycle-stacks-block-height)
        ))
        (map-get? participants { address: address })
      )))
      (stacking-address (get stacking-address participant-state))
      (stacking-state (unwrap-panic (map-get? stacking-holding {
        stacking-address: stacking-address,
        cycle-id: current-cycle-id,
      })))
      (stx-user-amount (get amount stacking-state))
      (tracking-address (get tracking-address participant-state))
      (tracking-state (unwrap-panic (map-get? tracking-holding {
        tracking-address: tracking-address,
        cycle-id: current-cycle-id,
      })))
      (sbtc-user-amount (get amount tracking-state))
      (current-proposed-ratio (var-get processing-ratio))
      (proposed-ratio-map (unwrap-panic (map-get? proposed-ratio {
        cycle-id: current-cycle-id,
        user: tx-sender,
      })))
      (proposed-participants-counted (get participants-counted proposed-ratio-map))
      (sbtc-below (get sbtc-below proposed-ratio-map))
      (sbtc-above (get sbtc-above proposed-ratio-map))
      (sbtc-equal (get sbtc-equal proposed-ratio-map))
    )
    (if (is-some (map-get? track-participant-computed-proposed-ratio {
        cycle-id: current-cycle-id,
        user: tx-sender,
        ratio: current-proposed-ratio,
        user-address: address,
      }))
      u0
      (begin
        (print {
          cycle-id: current-cycle-id,
          enrolled-address: address,
          stacking-address: stacking-address,
          tracking-address: tracking-address,
          function-name: "count-golden-ratio",
          stx-user-amount: stx-user-amount,
          sbtc-user-amount: sbtc-user-amount,
          tx-sender: tx-sender,
        })
        (map-set track-participant-computed-proposed-ratio {
          cycle-id: current-cycle-id,
          user: tx-sender,
          ratio: current-proposed-ratio,
          user-address: address,
        } { computed: true }
        )
        (map-set proposed-ratio {
          cycle-id: current-cycle-id,
          user: tx-sender,
        }
          (merge proposed-ratio-map { participants-counted: (+ proposed-participants-counted u1) })
        )
        (if (or
            (is-eq sbtc-user-amount u0)
            (is-some (map-get?
              track-participant-tracking-computed-proposed-ratio {
              cycle-id: current-cycle-id,
              user: tx-sender,
              ratio: current-proposed-ratio,
              user-tracking-address: tracking-address,
            }))
          )
          u0
          (let (
              (proposed-ratio-map-v2 (unwrap-panic (map-get? proposed-ratio {
                cycle-id: current-cycle-id,
                user: tx-sender,
              })))
              (user-ratio (/ (* stx-user-amount RATIO_PRECISION_HELPER_VALUE)
                sbtc-user-amount
              ))
            )
            (map-set track-participant-tracking-computed-proposed-ratio {
              cycle-id: current-cycle-id,
              user: tx-sender,
              ratio: current-proposed-ratio,
              user-tracking-address: tracking-address,
            } { computed: true }
            )
            (if (< user-ratio current-proposed-ratio)
              (begin
                (print {
                  user-address: address,
                  user-ratio: user-ratio,
                  action: "below",
                  user-amount: sbtc-user-amount,
                })
                (map-set proposed-ratio {
                  cycle-id: current-cycle-id,
                  user: tx-sender,
                }
                  (merge proposed-ratio-map-v2 { sbtc-below: (+ sbtc-below sbtc-user-amount) })
                )
              )
              (if (is-eq user-ratio current-proposed-ratio)
                (begin
                  (print {
                    user-address: address,
                    user-ratio: user-ratio,
                    action: "equal",
                    user-amount: sbtc-user-amount,
                  })
                  (map-set proposed-ratio {
                    cycle-id: current-cycle-id,
                    user: tx-sender,
                  }
                    (merge proposed-ratio-map-v2 { sbtc-equal: (+ sbtc-equal sbtc-user-amount) })
                  )
                )
                (begin
                  (print {
                    user-address: address,
                    user-ratio: user-ratio,
                    action: "above",
                    user-amount: sbtc-user-amount,
                  })
                  (map-set proposed-ratio {
                    cycle-id: current-cycle-id,
                    user: tx-sender,
                  }
                    (merge proposed-ratio-map-v2 { sbtc-above: (+ sbtc-above sbtc-user-amount) })
                  )
                )
              )
            )
            u0
          )
        )
      )
    )
  )
)

;; Update the percentage threshold for sBTC above the proposed ratio (admin only)
(define-public (set-max-percentage-above-ratio (new-max-percentage-above-ratio uint))
  (begin
    (asserts! (is-eq contract-caller (var-get admin)) ERR_NOT_ADMIN)
    (ok (var-set max-percentage-above-ratio new-max-percentage-above-ratio))
  )
)

;; Validate the proposed golden ratio based on participant distribution
(define-public (validate-ratio)
  (let (
      (proposed-ratio-map (unwrap!
        (map-get? proposed-ratio {
          cycle-id: (var-get cycle-id),
          user: tx-sender,
        })
        ERR_RATIO_NOT_PROPOSED
      ))
      (golden-ratio (get golden-ratio proposed-ratio-map))
      (participants-count (get participants-counted proposed-ratio-map))
      (sbtc-below (get sbtc-below proposed-ratio-map))
      (sbtc-above (get sbtc-above proposed-ratio-map))
      (sbtc-equal (get sbtc-equal proposed-ratio-map))
      (percentage-top-limit-ratio (/
        (* (var-get max-percentage-above-ratio)
          (var-get current-cycle-total-sbtc)
        )
        PRECISION_PERCENTAGE_RATIO
      ))
      (above-condition-respected (<= (* u100 sbtc-above) percentage-top-limit-ratio))
      (above-and-equal-condition-respected (>= (* u100 (+ sbtc-above sbtc-equal)) percentage-top-limit-ratio))
      (total-stx (var-get current-cycle-total-stx))
      (all-zero-stx (is-eq total-stx u0))
    )
    (print {
      sbtc-above: sbtc-above,
      sbtc-below: sbtc-below,
      sbtc-equal: sbtc-equal,
      sbtc-total: (var-get current-cycle-total-sbtc),
    })
    (asserts!
      (is-none (map-get? ratio-used { cycle-id: (var-get cycle-id) }))
      ERR_ALREADY_VALIDATED_RATIO
    )
    (asserts!
      (is-eq participants-count
        (var-get current-cycle-participants-count)
      )
      ERR_COMPUTE_RATIO_FOR_ALL_PARTICIPANTS_FIRST
    )

    ;; Note: Ratio of 0 is allowed (when all sBTC holders have no STX)
    ;; Division by zero is prevented in calculate functions by using max(D, MIN_GOLDEN_RATIO)
    ;; If no STX stacked anywhere, enforce D=1.0 (RATIO_PRECISION_HELPER_VALUE = 10^8)
    ;; This ensures baseline BTC-proportional distribution when no one stacks STX
    (asserts!
      (or (not all-zero-stx) (is-eq golden-ratio RATIO_PRECISION_HELPER_VALUE))
      ERR_INVALID_RATIO_FOR_ZERO_STX
    )

    ;; Golden ratio validation: ensures the proposed ratio represents a meaningful convergence point (5% of the sBTC enrolled)
    ;; 1. Ratio not too low: sbtc-above <= max% (not too many outliers above)
    ;; 2. Ratio not too high: (sbtc-above + sbtc-equal) >= max% (enough participants at/above)
    (if (not all-zero-stx)
      (begin
        (asserts! above-condition-respected ERR_RATIO_TOO_LOW)
        (asserts! above-and-equal-condition-respected
          ERR_RATIO_TOO_HIGH
        )
        false
      )
      false
    )

    (var-set is-ratio-validated true)
    (map-set ratio-used { cycle-id: (var-get cycle-id) } { used-ratio: golden-ratio })
    (var-set last-operation-done u3)
    ;; ratio-validated
    (ok true)
  )
)

;; Get ratio validation data for the current cycle
(define-read-only (get-ratio-data)
  (let ((proposed-ratio-map (unwrap!
      (map-get? proposed-ratio {
        cycle-id: (var-get cycle-id),
        user: tx-sender,
      })
      ERR_RATIO_NOT_PROPOSED
    )))
    (ok {
      sbtc-total-totals: (var-get current-cycle-total-sbtc),
      nr-snapshots: (var-get snapshots-per-cycle),
      sbtc-below: (get sbtc-below proposed-ratio-map),
      sbtc-above: (get sbtc-above proposed-ratio-map),
    })
  )
)

;; ===================================
;; WEIGHT COMPUTATION FUNCTIONS
;; ===================================

;; Calculate weights for all participants using the dual stacking formula
;; Must be called after validate-ratio and before set-is-distribution-enabled
(define-public (calculate-participant-weights (principals (list 900 principal)))
  (let (
      (stx-id-header-hash (unwrap!
        (get-stacks-block-info? id-header-hash
          (var-get current-cycle-stacks-block-height)
        )
        ERR_STX_BLOCK_IN_FUTURE
      ))
      (cycle-id-current (var-get cycle-id))
      (golden-ratio-data (unwrap! (map-get? ratio-used { cycle-id: cycle-id-current })
        ERR_RATIOS_NOT_CONCLUDED
      ))
      (D-raw (get used-ratio golden-ratio-data)) ;; This is the validated 95th percentile ratio
      ;; Apply minimum to prevent division by zero when all sBTC holders have 0 STX
      (D (if (> D-raw u0)
        D-raw
        MIN_GOLDEN_RATIO
      ))
    )
    (asserts! (var-get is-ratio-validated) ERR_RATIOS_NOT_CONCLUDED)
    (var-set snapshot-block-hash stx-id-header-hash)
    (let ((result (fold calculate-participant-weight principals {
        D: D,
        total: u0,
      })))
      (var-set total-weights-sum
        (+ (var-get total-weights-sum) (get total result))
      )
      (ok result)
    )
  )
)

;; Calculate weight for a single user and accumulate to total
(define-private (calculate-participant-weight
    (address principal)
    (acc {
      D: uint,
      total: uint,
    })
  )
  (let (
      (cycle-id-current (var-get cycle-id))
      (participant-state (unwrap-panic (at-block
        (unwrap-panic (get-stacks-block-info? id-header-hash
          (var-get current-cycle-stacks-block-height)
        ))
        (map-get? participants { address: address })
      )))
      (stacking-address (get stacking-address participant-state))
      (tracking-address (get tracking-address participant-state))
      (stacking-state (unwrap-panic (map-get? stacking-holding {
        stacking-address: stacking-address,
        cycle-id: cycle-id-current,
      })))
      (tracking-state (unwrap-panic (map-get? tracking-holding {
        tracking-address: tracking-address,
        cycle-id: cycle-id-current,
      })))
      (Si (get amount stacking-state)) ;; STX stacked by user
      (Bi (get amount tracking-state)) ;; sBTC held by user
      (M (var-get yield-boost-multiplier)) ;; Multiplier (default 9 for 10x max)
      (D (get D acc))
    )
    (if ;; already computed weight for this enrolled participant
      (is-some (map-get? participant-weight-tracked {
        cycle-id: cycle-id-current,
        enrolled-address: address,
      }))
      acc
      (if (is-some (map-get? participant-weights {
          cycle-id: cycle-id-current,
          tracking-address: tracking-address,
        }))
        (begin
          (var-set weights-computed-count
            (+ (var-get weights-computed-count) u1)
          )
          (print {
            cycle-id: cycle-id-current,
            enrolled-address: address,
            tracking-address: tracking-address,
            Bi: u0,
            weight: u0,
            function-name: "compute-weight",
          })
          acc
        )
        (if (is-eq Bi u0)
          ;; User has no sBTC, just set its map value and increase count
          (begin
            (map-set participant-weights {
              cycle-id: cycle-id-current,
              tracking-address: tracking-address,
            } { weight: u0 }
            )
            (map-set participant-weight-tracked {
              cycle-id: cycle-id-current,
              enrolled-address: address,
            } { computed: true }
            )
            (var-set weights-computed-count
              (+ (var-get weights-computed-count) u1)
            )
            (print {
              cycle-id: cycle-id-current,
              enrolled-address: address,
              tracking-address: tracking-address,
              Bi: u0,
              weight: u0,
              function-name: "compute-weight",
            })
            acc
          )
          (let (
              ;; Check if this tracking address is whitelisted for max boost
              (is-whitelisted (is-some (map-get? whitelisted-defi-tracking-addresses { address: tracking-address })))
              ;; Calculate di = Si/Bi (scaled by 10^8)
              (di (/ (* Si RATIO_PRECISION_HELPER_VALUE) Bi))
              ;; Calculate ri = min(di/D, 1) (scaled by 10^8)
              ;; If whitelisted, use ri = 1.0 for maximum boost
              ;; Note: D should already be >= MIN_GOLDEN_RATIO from calculate-participant-weights
              (ri (if is-whitelisted
                RATIO_PRECISION_HELPER_VALUE
                (if (< D MIN_GOLDEN_RATIO)
                  u0
                  (if (> di D)
                    RATIO_PRECISION_HELPER_VALUE
                    (/ (* di RATIO_PRECISION_HELPER_VALUE) D)
                  )
                )
              ))
              ;; Calculate sqrt(ri) using integer square root
              (sqrt-ri (sqrti ri))
              ;; Calculate wi = Bi * (1 + M * sqrt-ri)
              ;; wi = Bi * (10^8 + M * sqrt-ri * 10^4) / 10^8 / snapshots-per-cycle
              (weight (/
                (/
                  (* Bi
                    (+ RATIO_PRECISION_HELPER_VALUE
                      (* M sqrt-ri SQRT_RATIO_PRECISION_HELPER_VALUE)
                    ))
                  RATIO_PRECISION_HELPER_VALUE
                )
                (var-get snapshots-per-cycle)
              ))
            )
            ;; Store the weight for this tracking address
            (map-set participant-weights {
              cycle-id: cycle-id-current,
              tracking-address: tracking-address,
            } { weight: weight }
            )
            (map-set participant-weight-tracked {
              cycle-id: cycle-id-current,
              enrolled-address: address,
            } { computed: true }
            )
            (var-set weights-computed-count
              (+ (var-get weights-computed-count) u1)
            )
            (print {
              cycle-id: cycle-id-current,
              enrolled-address: address,
              tracking-address: tracking-address,
              Bi: Bi,
              Si: Si,
              di: di,
              D: D,
              ri: ri,
              sqrt-ri: sqrt-ri,
              weight: weight,
              is-whitelisted: is-whitelisted,
              function-name: "compute-weight",
            })
            ;; Accumulate total weight
            {
              D: D,
              total: (+ (get total acc) weight),
            }
          )
        )
      )
    )
  )
)

;; Finalize weight computation - stores the total
(define-public (finalize-weight-computation)
  (let ((cycle-id-current (var-get cycle-id)))
    (asserts! (var-get is-ratio-validated) ERR_RATIOS_NOT_CONCLUDED)
    (asserts! (not (var-get are-weights-computed))
      ERR_WEIGHTS_ALREADY_COMPUTED
    )
    (asserts!
      (is-eq (var-get weights-computed-count)
        (var-get current-cycle-participants-count)
      )
      ERR_NOT_ALL_WEIGHTS_COMPUTED
    )
    ;; Note: total-weights-sum should be set by summing during compute calls
    ;; This is a checkpoint to mark weights as computed
    (var-set are-weights-computed true)
    (print {
      cycle-id: cycle-id-current,
      total-weights-sum: (var-get total-weights-sum),
      function-name: "finalize-weights",
    })
    (var-set last-operation-done u4)
    ;; finalize-weight-computation
    (ok true)
  )
)

(define-read-only (get-weight-computation-status)
  {
    are-weights-computed: (var-get are-weights-computed),
    total-weights-sum: (var-get total-weights-sum),
    cycle-id: (var-get cycle-id),
  }
)

(define-read-only (get-participant-weight
    (cycle uint)
    (tracking-addr principal)
  )
  (map-get? participant-weights {
    cycle-id: cycle,
    tracking-address: tracking-addr,
  })
)

;; ===================================
;; REWARD DISTRIBUTION FUNCTIONS
;; ===================================

;; Enable reward distribution after validating cycle completion
(define-public (set-is-distribution-enabled)
  (begin
    (asserts! (var-get is-contract-active) ERR_CONTRACT_NOT_ACTIVE)
    (asserts! (not (var-get is-distribution-enabled))
      ERR_SET_CAN_DISTRIBUTE_ALREADY_CALLED
    )
    (asserts! (var-get are-weights-computed) ERR_WEIGHTS_NOT_COMPUTED)
    (let (
        (pool-rewards (at-block
          (unwrap-panic (get-stacks-block-info? id-header-hash
            (var-get current-snapshot-stacks-block-height)
          ))
          (unwrap-panic (contract-call?
            'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token
            get-balance-available (as-contract contract-caller)
          ))
        ))
        ;; This is the real value
        (max-cap-rewards (/ (* (cycle-percentage-rate) (var-get total-weights-sum))
          (+ (var-get yield-boost-multiplier) u1)
        ))
        (local-rewards-to-distribute (if (< (* SCALING_FACTOR pool-rewards) max-cap-rewards)
          (* SCALING_FACTOR pool-rewards)
          max-cap-rewards
        ))
      )
      (var-set rewards-to-distribute local-rewards-to-distribute)
      (var-set last-operation-done u5)
      ;; set-can-distribute
      (ok (var-set is-distribution-enabled true))
    )
  )
)

;; Distribute rewards to a list of participants
(define-public (distribute-rewards (principals (list 900 principal)))
  (let ((stx-id-header-hash (unwrap!
      (get-stacks-block-info? id-header-hash
        (var-get current-cycle-stacks-block-height)
      )
      ERR_STX_BLOCK_IN_FUTURE
    )))
    (var-set snapshot-block-hash stx-id-header-hash)
    (asserts! (var-get is-distribution-enabled)
      ERR_CANNOT_DISTRIBUTE_REWARDS
    )
    (if (is-eq (var-get current-cycle-total-sbtc) u0)
      (begin
        (var-set participants-rewarded-count
          (var-get current-cycle-participants-count)
        )
        (ok (list))
      )
      (ok (map distribute-reward-user principals))
    )
  )
)

;; Distribute reward to a single user based on their computed weight
;; Note: Multiple enrolled addresses can share the same tracking/stacking/rewarded address
;; Rewards are aggregated and sent once per unique tracking-address to avoid duplicates
(define-private (distribute-reward-user (user principal))
  (let (
      (cycle-id-current (var-get cycle-id))
      (holding-state (unwrap!
        (map-get? participant-holding {
          address: user,
          cycle-id: cycle-id-current,
        })
        ERR_NOT_ENROLLED
      ))
      (participant-state (unwrap-panic (at-block
        (unwrap-panic (get-stacks-block-info? id-header-hash
          (var-get current-cycle-stacks-block-height)
        ))
        (map-get? participants { address: user })
      )))
      (stacking-address (get stacking-address participant-state))
      (stacking-state (unwrap!
        (map-get? stacking-holding {
          stacking-address: stacking-address,
          cycle-id: cycle-id-current,
        })
        ERR_NOT_ENROLLED
      ))
      (tracking-address (get tracking-address participant-state))
      (tracking-state (unwrap!
        (map-get? tracking-holding {
          tracking-address: tracking-address,
          cycle-id: cycle-id-current,
        })
        ERR_NOT_ENROLLED
      ))
      (rewarded-address (get rewarded-address participant-state))
      (already-rewarded-amount (default-to u0
        (get amount
          (map-get? rewards-holding {
            rewarded-address: rewarded-address,
            cycle-id: cycle-id-current,
          })
        )))
      (weight-data (unwrap!
        (map-get? participant-weights {
          cycle-id: cycle-id-current,
          tracking-address: tracking-address,
        })
        ERR_WEIGHTS_NOT_COMPUTED
      ))
      (user-weight (get weight weight-data))
      (total-weight (var-get total-weights-sum))
      (stx-user-amount (get amount stacking-state))
      (sbtc-user-amount (get amount tracking-state))
      (user-theoretical-ratio (if (is-eq sbtc-user-amount u0)
        u0
        (/ (* stx-user-amount RATIO_PRECISION_HELPER_VALUE)
          sbtc-user-amount
        )
      ))
      (golden-ratio (unwrap!
        (get used-ratio
          (map-get? ratio-used { cycle-id: cycle-id-current })
        )
        ERR_RATIOS_NOT_CONCLUDED
      ))
      (is-whitelisted (get-is-whitelisted-defi tracking-address))
      (user-ratio (if (or is-whitelisted (> user-theoretical-ratio golden-ratio))
        golden-ratio
        user-theoretical-ratio
      ))
      (reward-amount (if (is-eq total-weight u0)
        u0
        (/ (* user-weight (var-get rewards-to-distribute))
          (* total-weight SCALING_FACTOR)
        )
      ))
    )
    (if (get rewarded holding-state)
      ERR_ALREADY_REWARDED
      (begin
        (var-set participants-rewarded-count
          (+ (var-get participants-rewarded-count) u1)
        )
        (map-set participant-holding {
          address: user,
          cycle-id: cycle-id-current,
        }
          (merge holding-state {
            rewarded: true,
            reward-amount: reward-amount,
          })
        )
        ;; if the tracking address linked to the current holding address was already rewarded 
        ;; -> consider current holding address as rewarded as well
        (if (not (get rewarded tracking-state)) ;; is false and it wasn't already rewarded
          (begin
            (map-set rewards-holding {
              rewarded-address: rewarded-address,
              cycle-id: cycle-id-current,
            } { amount: (+ already-rewarded-amount reward-amount) }
            )
            (map-set tracking-holding {
              tracking-address: tracking-address,
              cycle-id: cycle-id-current,
            }
              (merge tracking-state { rewarded: true })
            )
            (print {
              cycle-id: cycle-id-current,
              enrolled-address: user,
              reward-address: rewarded-address,
              stacking-address: stacking-address,
              tracking-address: tracking-address,
              amount: reward-amount,
              weight: user-weight,
              ratio: user-ratio,
              function-name: "distribute-rewards",
            })
            (if (> reward-amount u0)
              (try! (as-contract (contract-call?
                'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token
                transfer reward-amount tx-sender rewarded-address
                none
              )))
              false
            )
          )
          (begin
            (print {
              cycle-id: cycle-id-current,
              enrolled-address: user,
              reward-address: rewarded-address,
              stacking-address: stacking-address,
              tracking-address: tracking-address,
              amount: u0,
              weight: u0,
              ratio: u0,
              function-name: "distribute-rewards",
            })
            false ;; does nothing
          )
        )
        (ok true)
      )
    )
  )
)

;; Finalize reward distribution for the current cycle
(define-public (finalize-reward-distribution)
  (begin
    (asserts! (var-get is-contract-active) ERR_CONTRACT_NOT_ACTIVE)
    (asserts!
      (is-eq (var-get current-cycle-participants-count)
        (var-get participants-rewarded-count)
      )
      ERR_NOT_REWARDED_ALL
    )
    (asserts! (var-get is-distribution-enabled) ERR_CANNOT_FINALIZE)
    (asserts!
      (is-none (map-get? distribution-finalized-stx-block-height-when-called { cycle-id: (var-get cycle-id) }))
      ERR_ALREADY_FINALIZED
    )

    (print {
      bitcoin-block-height: burn-block-height,
      cycle-id: (var-get cycle-id),
      function-name: "finalize-reward-distribution",
    })
    (var-set last-operation-done u6) ;; finalized
    ;; Used by DeFi apps to call their rewards distribution afterward it is finalized here
    (ok (map-set distribution-finalized-stx-block-height-when-called { cycle-id: (var-get cycle-id) } { stx-block-height: stacks-block-height }))
  )
)

;; ===================================
;; CYCLE MANAGEMENT FUNCTIONS
;; ===================================

;; Advance to the next cycle
(define-public (advance-to-next-cycle (stx-block-height uint))
  (begin
    (asserts! (var-get is-contract-active) ERR_CONTRACT_NOT_ACTIVE)
    (asserts!
      (>= burn-block-height (var-get next-cycle-bitcoin-block-height))
      ERR_NOT_NEW_CYCLE_YET
    )
    (asserts!
      (is-eq (var-get current-cycle-participants-count)
        (var-get participants-rewarded-count)
      )
      ERR_NOT_REWARDED_ALL
    )
    (asserts!
      (is-some (map-get? distribution-finalized-stx-block-height-when-called { cycle-id: (var-get cycle-id) }))
      ERR_REWARDS_NOT_SENT_YET
    )
    (asserts!
      (unwrap-panic (contract-call? .bitcoin-block-buffer
        validate-stx-block-brackets-btc-block stx-block-height
        (var-get next-cycle-bitcoin-block-height)
      ))
      ERR_STX_BLOCK_NOT_MATCHING
    )

    (var-set current-cycle-bitcoin-block-height
      (+ (var-get current-cycle-bitcoin-block-height)
        (* (var-get blocks-per-snapshot) (var-get snapshots-per-cycle))
      ))
    (var-set cycle-id (+ (var-get cycle-id) u1))
    (reset-state-for-cycle stx-block-height)
    (var-set next-cycle-bitcoin-block-height
      (+ (var-get current-cycle-bitcoin-block-height)
        (* (var-get blocks-per-snapshot) (var-get snapshots-per-cycle))
      ))
    (update-snapshot-for-new-cycle stx-block-height)

    (ok (map-set cycle-snapshot-to-stx-block-height {
      cycle-id: (var-get cycle-id),
      snapshot-id: (var-get current-snapshot-index),
    } {
      stx-block-height: stx-block-height,
      bitcoin-block-height-stored: (var-get current-cycle-bitcoin-block-height),
    }))
  )
)

;; ===================================
;; READ-ONLY FUNCTIONS - Cycle Info
;; ===================================

(define-read-only (get-current-cycle-id)
  (var-get cycle-id)
)

(define-read-only (cycle-data)
  {
    cycle-id: (var-get cycle-id),
    current-cycle-bitcoin-block-height: (var-get current-cycle-bitcoin-block-height),
    next-cycle-bitcoin-block-height: (var-get next-cycle-bitcoin-block-height),
    current-cycle-stacks-block-height: (var-get current-cycle-stacks-block-height),
    participants-count: (var-get current-cycle-participants-count),
    snapshots-per-cycle: (var-get snapshots-per-cycle),
    blocks-per-snapshot: (var-get blocks-per-snapshot),
    current-snapshot-index: (var-get current-snapshot-index),
  }
)

(define-read-only (get-cycle-current-state)
  {
    cycle-id: (var-get cycle-id),
    first-bitcoin-block: (var-get current-cycle-bitcoin-block-height),
    last-bitcoin-block: (- (var-get next-cycle-bitcoin-block-height) u1),
  }
)

(define-read-only (current-overview-data)
  {
    cycle-id: (var-get cycle-id),
    snapshot-index: (var-get current-snapshot-index),
    snapshots-per-cycle: (var-get snapshots-per-cycle),
  }
)

(define-read-only (get-yield-cycle-data (cycle uint))
  (map-get? yield-cycles-data { cycle-id: cycle })
)

;; ===================================
;; READ-ONLY FUNCTIONS - Snapshot Info
;; ===================================

(define-read-only (snapshot-data)
  {
    current-snapshot-bitcoin-block-height: (var-get current-snapshot-bitcoin-block-height),
    current-snapshot-stacks-block-height: (var-get current-snapshot-stacks-block-height),
    current-snapshot-count: (var-get current-snapshot-count),
    current-snapshot-total-sbtc: (var-get current-snapshot-total-sbtc),
    current-snapshot-total-stx: (var-get current-snapshot-total-stx),
    current-snapshot-index: (var-get current-snapshot-index),
    blocks-per-snapshot: (var-get blocks-per-snapshot),
  }
)

(define-read-only (get-stacks-block-height-for-cycle-snapshot
    (checked-cycle-id uint)
    (checked-snapshot-id uint)
  )
  (get stx-block-height
    (map-get? cycle-snapshot-to-stx-block-height {
      cycle-id: checked-cycle-id,
      snapshot-id: checked-snapshot-id,
    })
  )
)

(define-read-only (get-bitcoin-block-height-for-cycle-snapshot
    (checked-cycle-id uint)
    (checked-snapshot-id uint)
  )
  (get bitcoin-block-height-stored
    (map-get? cycle-snapshot-to-stx-block-height {
      cycle-id: checked-cycle-id,
      snapshot-id: checked-snapshot-id,
    })
  )
)

;; ===================================
;; READ-ONLY FUNCTIONS - Reward Info
;; ===================================

(define-read-only (get-reward-distribution-status)
  {
    participants-rewarded-count: (var-get participants-rewarded-count),
    is-distribution-enabled: (var-get is-distribution-enabled),
  }
)

(define-read-only (is-distribution-ready)
  (var-get is-distribution-enabled)
)

(define-read-only (reward-amount-for-cycle-and-address
    (wanted-cycle-id uint)
    (address principal)
  )
  (get reward-amount
    (map-get? participant-holding {
      cycle-id: wanted-cycle-id,
      address: address,
    })
  )
)

(define-read-only (reward-amount-for-cycle-and-reward-address
    (wanted-cycle-id uint)
    (reward-address principal)
  )
  (get amount
    (map-get? rewards-holding {
      cycle-id: wanted-cycle-id,
      rewarded-address: reward-address,
    })
  )
)

(define-read-only (is-distribution-finalized-for-current-cycle)
  (is-some (map-get? distribution-finalized-stx-block-height-when-called { cycle-id: (var-get cycle-id) }))
)

(define-read-only (get-distribution-finalized-at-height (wanted-cycle-id uint))
  (map-get? distribution-finalized-stx-block-height-when-called { cycle-id: wanted-cycle-id })
)

;; ===================================
;; READ-ONLY FUNCTIONS - State & Config
;; ===================================

(define-read-only (get-last-operation-state)
  (var-get last-operation-done)
)

(define-read-only (get-admin)
  (var-get admin)
)

(define-read-only (get-is-contract-active)
  (var-get is-contract-active)
)

(define-read-only (get-current-bitcoin-block-height)
  burn-block-height
)

(define-read-only (get-minimum-enrollment-amount)
  (var-get min-sbtc-hold-required-for-enrollment)
)

;; If SC not initialized, returns bitcoin-block block height for it
;; else returns the bitcoin-block block height for next snapshot/cycle
(define-read-only (get-next-action-bitcoin-height)
  (if (var-get is-contract-active)
    (+ (var-get current-snapshot-bitcoin-block-height)
      (var-get blocks-per-snapshot)
    )
    (var-get current-cycle-bitcoin-block-height)
  )
)

;; ===================================
;; READ-ONLY FUNCTIONS - Participant Info
;; ===================================

(define-read-only (is-enrolled-in-next-cycle (address principal))
  (is-some (map-get? participants { address: address }))
)

(define-read-only (is-enrolled-this-cycle (address principal))
  (is-some (at-block
    (unwrap-panic (get-stacks-block-info? id-header-hash
      (var-get current-cycle-stacks-block-height)
    ))
    (map-get? participants { address: address })
  ))
)

(define-read-only (get-is-blacklisted (address principal))
  (is-some (map-get? blacklist { address: address }))
)

(define-read-only (get-is-blacklisted-list (addresses (list 900 principal)))
  (map is-blacklisted addresses)
)

(define-read-only (get-is-whitelisted-defi (address principal))
  (is-some (map-get? whitelisted-defi-tracking-addresses { address: address }))
)

;; Helper for blacklist checking
(define-private (is-blacklisted (address principal))
  (is-some (map-get? blacklist { address: address }))
)

(define-read-only (get-latest-reward-address (address principal))
  (get rewarded-address (map-get? participants { address: address }))
)

(define-read-only (get-participant-cycle-info
    (address principal)
    (cycle uint)
  )
  (let (
      (participant-info (at-block
        (unwrap-panic (get-stacks-block-info? id-header-hash
          (unwrap!
            (get start-stx-block-height
              (map-get? yield-cycles-data { cycle-id: cycle })
            )
            ERR_CYCLE_IN_FUTURE
          )))
        (map-get? participants { address: address })
      ))
      (tracking-address (unwrap! (get tracking-address participant-info) ERR_NOT_ENROLLED))
      (stacking-address (unwrap! (get stacking-address participant-info) ERR_NOT_ENROLLED))
    )
    (ok {
      participant-data: participant-info,
      holding-data: (map-get? participant-holding {
        cycle-id: cycle,
        address: address,
      }),
      tracking-data: (map-get? tracking-holding {
        cycle-id: cycle,
        tracking-address: tracking-address,
      }),
      stacking-data: (map-get? stacking-holding {
        cycle-id: cycle,
        stacking-address: stacking-address,
      }),
    })
  )
)

;; ===================================
;; READ-ONLY FUNCTIONS - APR Calculations
;; ===================================

;; Number of cycles per year - average number of blocks in a year / number of
;; blocks in a cycle. When blocks-per-snapshot or next-snapshots-per-cycle are
;; updated, this value is recalculated.
(define-read-only (nr-cycles-year)
  (/ (var-get bitcoin-blocks-per-year)
    (* (var-get blocks-per-snapshot) (var-get snapshots-per-cycle))
  )
)

;; Cycle percentage rate scaled by 10^8. This represents the maximum
;; percentage of the sBTC yield contributors are ready to give away as rewards
;; in a cycle.
;; CPR = APR / nr-cycles-per-year
;; When APR or nr-cycles-per-year are updated, this value is recalculated.
(define-read-only (cycle-percentage-rate)
  (/ (var-get APR) (nr-cycles-year))
)

;; ===================================
;; READ-ONLY FUNCTIONS - Validation
;; ===================================

(define-read-only (get-amount-stx-stacked (address principal))
  (get locked (stx-account address))
)

(define-read-only (get-amount-stx-stacked-at-block-height
    (address principal)
    (stx-block-height uint)
  )
  (at-block
    (unwrap-panic (get-stacks-block-info? id-header-hash stx-block-height))
    (get locked (stx-account address))
  )
)

(define-read-only (get-amount-stacked-at-block-height-mainnet
    (address principal)
    (stx-block-height uint)
  )
  (if (var-get is-liquid-stacking-enabled)
    (+
      (contract-call? .liquid-stacking-v1_0_1
        get-user-liquid-stx-stacked-at-block-height address
        stx-block-height
      )
      (get-amount-stx-stacked-at-block-height address stx-block-height)
    )
    (get-amount-stx-stacked-at-block-height address stx-block-height)
  )
)

;; Get total amount of STX stacked (native + liquid stacking if enabled) at given block-height
(define-read-only (get-amount-stacked-at-block-height
    (address principal)
    (stx-block-height uint)
  )
  (get-amount-stacked-at-block-height-mainnet address stx-block-height)
)

(define-read-only (get-amount-stacked-now (address principal))
  (get-amount-stacked-at-block-height address
    (- stacks-block-height u1)
  )
)

(define-read-only (get-apr-data)
  {
    MULTIPLIER: (+ (var-get yield-boost-multiplier) u1),
    MAX_APR: (var-get APR),
    MIN_APR: (/ (var-get APR) (+ (var-get yield-boost-multiplier) u1)),
  }
)

Functions (73)

FunctionAccessArgs
update-initialize-blockpublicnew-bitcoin-block-height: uint
initialize-contractpublicstx-block-height: uint
reset-state-for-cycleprivatestx-block-height: uint
update-snapshot-for-new-cycleprivatestx-block-height: uint
enrollpublicrewarded-address: (optional principal
enroll-defipublicdefi-contract: principal, tracking-address: principal, rewarded-address: principal, stacking-address: (optional principal
opt-outpublic
opt-out-defipublicdefi-contract: principal
opt-out-defi-batchpublicdefi-contracts: (list 200 principal
whitelist-defi-trackingpublicdefi-rewards-contract: principal
remove-whitelisted-defi-trackingpublicdefi-rewards-contract: principal
remove-whitelisted-defi-tracking-batchpublicdefi-rewards-contract: (list 200 principal
remove-participantprivateaddress: principal
set-liquid-stackingpublicstate: bool
change-reward-addresspublicnew-address: principal
change-addresses-defipublicdefi-contract: principal, new-tracking-address: (optional principal
add-blacklistedpublicaddress: principal
add-blacklisted-batchpublicaddresses: (list 200 principal
remove-blacklistedpublicaddress: principal
remove-blacklisted-batchpublicaddresses: (list 200 principal
update-adminpublicnew-admin-address: principal
update-min-sbtc-hold-required-for-enrollmentpublicnew-min-hold: uint
update-snapshot-lengthpublicupdated-blocks-per-snapshot: uint
update-snapshots-per-cyclepublicupdated-snapshots-per-cycle: uint
update-bitcoin-blocks-per-yearpublicupdated-bitcoin-blocks-per-year: uint
update-APRpublicnew-apr-one-eight: uint
update-yield-boost-multiplierpublicnew-multiplier: uint
get-contract-sbtc-balanceread-only
capture-snapshot-balancespublicprincipals: (list 900 principal
advance-to-next-snapshotpublicnew-stx-block-height: uint
finalize-snapshotspublic
propose-golden-ratiopublicratio: uint
change-proposed-golden-ratiopublicratio: uint
tally-participant-ratiospublicprincipals: (list 900 principal
set-max-percentage-above-ratiopublicnew-max-percentage-above-ratio: uint
validate-ratiopublic
get-ratio-dataread-only
calculate-participant-weightspublicprincipals: (list 900 principal
finalize-weight-computationpublic
get-weight-computation-statusread-only
set-is-distribution-enabledpublic
distribute-rewardspublicprincipals: (list 900 principal
distribute-reward-userprivateuser: principal
finalize-reward-distributionpublic
advance-to-next-cyclepublicstx-block-height: uint
get-current-cycle-idread-only
cycle-dataread-only
get-cycle-current-stateread-only
current-overview-dataread-only
get-yield-cycle-dataread-onlycycle: uint
snapshot-dataread-only
get-reward-distribution-statusread-only
is-distribution-finalized-for-current-cycleread-only
get-distribution-finalized-at-heightread-onlywanted-cycle-id: uint
get-last-operation-stateread-only
get-adminread-only
get-is-contract-activeread-only
get-current-bitcoin-block-heightread-only
get-minimum-enrollment-amountread-only
get-next-action-bitcoin-heightread-only
is-enrolled-in-next-cycleread-onlyaddress: principal
is-enrolled-this-cycleread-onlyaddress: principal
get-is-blacklistedread-onlyaddress: principal
get-is-blacklisted-listread-onlyaddresses: (list 900 principal
get-is-whitelisted-defiread-onlyaddress: principal
is-blacklistedprivateaddress: principal
get-latest-reward-addressread-onlyaddress: principal
nr-cycles-yearread-only
cycle-percentage-rateread-only
get-amount-stx-stackedread-onlyaddress: principal
get-amount-stacked-nowread-onlyaddress: principal
get-apr-dataread-only
is-distribution-readyread-only