Source Code

;; TITLE: daily-caps-module
;; VERSION: 1.0

(use-trait token-trait .trait-sip-010.sip-010-trait)

;; CONSTANTS
(define-constant LP-CONTRACT (as-contract .liquidity-provider-v1))
(define-constant BORROWER-CONTRACT (as-contract .borrower-v1))
(define-constant SUCCESS (ok true))
(define-constant SCALING-FACTOR u100000000)
(define-constant ACTION_SET_LP_CAP u0)
(define-constant ACTION_SET_DEBT_CAP u1)
(define-constant ACTION_SET_COLLATERAL_CAP u2)
(define-constant ACTION_SET_TIME_WINDOW u3)
;; Threshold to either execute or remove proposal
(define-constant THRESHOLD u60) ;; 60% and above

;; ERRORS
(define-constant ERR-RESTRICTED (err u90000))
(define-constant ERR-FAILED-TO-GET-BALANCE (err u90001))
(define-constant ERR-DAILY-LP-CAP-EXCEEDED (err u90002))
(define-constant ERR-DAILY-DEBT-CAP-EXCEEDED (err u90003))
(define-constant ERR-DAILY-COLLATERAL-CAP-EXCEEDED (err u90004))
(define-constant ERR-INVALID-CAP-FACTOR (err u90005))
;; governance-related errors
(define-constant ERR-INVALID-ACTION (err u90006))
(define-constant ERR-UNKNOWN-PROPOSAL (err u90007))
(define-constant ERR-SUBMITTED-VOTE (err u90008))
(define-constant ERR-PROPOSAL-COMPLETED (err u90009))
(define-constant ERR-CONTRACT-ALREADY-INITIALIZED (err u90010))
(define-constant ERR-CONTRACT-NOT-INITIALIZED (err u90011))
(define-constant ERR-NOT-CONTRACT-DEPLOYER (err u90012))
(define-constant ERR-PROPOSAL-ALREADY-EXISTS (err u90013))
(define-constant ERR-FAILED-TO-GENERATE-PROPOSAL-ID (err u90014))
(define-constant ERR-PROPOSAL-VOTING-INCOMPLETE (err u90015))
(define-constant ERR-PROPOSAL-CANNOT-CLOSE (err u90016))
(define-constant ERR-PROPOSAL-EXPIRED (err u90017))

;; VARIABLES

(define-data-var time-window uint u86400)

;; LP
(define-data-var lp-cap-factor uint u0)
(define-data-var last-lp-bucket-update uint u0)
(define-data-var lp-bucket uint u0) ;; current available lp withdrawal credit

;; Debt
(define-data-var debt-cap-factor uint u0)
(define-data-var last-debt-bucket-update uint u0)
(define-data-var debt-bucket uint u0) ;; current available debt borrowing credit

;; Collateral
(define-map last-collateral-bucket-update principal uint)
(define-map collateral-bucket principal uint) ;; current available collateal withdrawal credit
(define-map collateral-cap-factor principal uint)

;; Governance
(define-data-var next-proposal-nonce uint u0)
(define-map governance-proposal (buff 32) {
  action: uint,
  approve-count: uint,
  deny-count: uint,
  completed: bool,
  expires-at: uint,
})
(define-map proposal-approved-members { proposal-id: (buff 32), member: principal} bool)
(define-map proposal-denied-members { proposal-id: (buff 32), member: principal} bool)
(define-map lp-cap-data (buff 32) uint)
(define-map debt-cap-data (buff 32) uint)
(define-map collateral-cap-data (buff 32) {
    collateral: principal,
    factor: uint
  }
)
(define-map time-window-data (buff 32) uint)

(define-read-only (get-time-window) (var-get time-window))

(define-read-only (get-lp-cap-factor) (var-get lp-cap-factor))
(define-read-only (get-last-lp-bucket-update) (var-get last-lp-bucket-update))
(define-read-only (get-lp-bucket) (var-get lp-bucket))

(define-read-only (get-debt-cap-factor) (var-get debt-cap-factor))
(define-read-only (get-last-debt-bucket-update) (var-get last-debt-bucket-update))
(define-read-only (get-debt-bucket) (var-get debt-bucket))

(define-read-only (get-collateral-cap-factor (collateral <token-trait>)) (default-to u0 (map-get? collateral-cap-factor (contract-of collateral))))
(define-read-only (get-last-collateral-bucket-update (collateral <token-trait>)) (default-to u0 (map-get? last-collateral-bucket-update (contract-of collateral))))
(define-read-only (get-collateral-bucket (collateral <token-trait>)) (default-to u0 (map-get? collateral-bucket (contract-of collateral))))

(define-read-only (min (a uint) (b uint)) (if (> a b) b a ))

(define-read-only (is-governance-member (member principal))
  (contract-call? .meta-governance-v1 is-governance-member member)
)

(define-read-only (get-proposal (proposal-id (buff 32)))
  (map-get? governance-proposal proposal-id)
)

;; PRIVATE FUNCTIONS

(define-private (sync-lp-bucket)
  (let
    (
      (cap-reset-time (var-get time-window))
      (time-now (unwrap-panic (get-stacks-block-info? time (- stacks-block-height u1))))
      (last-ts (var-get last-lp-bucket-update))
      (elapsed (if (is-eq last-ts u0) cap-reset-time (- time-now last-ts)))
      (total-liquidity (unwrap! (contract-call? .mock-usdc get-balance .state-v1) ERR-FAILED-TO-GET-BALANCE))
      (max-lp-bucket (/ (* total-liquidity (var-get lp-cap-factor)) SCALING-FACTOR))
      (refill-amount (if (>= elapsed cap-reset-time) 
                     max-lp-bucket
                     (/ (* max-lp-bucket elapsed) cap-reset-time)))
      (current-bucket (var-get lp-bucket))
      (new-bucket-value (min (+ current-bucket refill-amount) max-lp-bucket))
    )
    (print {
      old-lp-bucket-value: (var-get lp-bucket),
      new-lp-bucket-value: new-bucket-value,
      sender: contract-caller,
      action: "sync-lp-bucket"
    })
    (var-set lp-bucket new-bucket-value)
    (var-set last-lp-bucket-update time-now)
    SUCCESS
  )
)

(define-private (sync-debt-bucket)
  (let
    (
      (cap-reset-time (var-get time-window))
      (time-now (unwrap-panic (get-stacks-block-info? time (- stacks-block-height u1))))
      (last-ts (var-get last-debt-bucket-update))
      (elapsed (if (is-eq last-ts u0) cap-reset-time (- time-now last-ts)))
      (total-liquidity (contract-call? .state-v1 get-borrowable-balance))
      (max-debt-bucket (/ (* total-liquidity (var-get debt-cap-factor)) SCALING-FACTOR))
      (refill-amount (if (>= elapsed cap-reset-time) 
                     max-debt-bucket
                     (/ (* max-debt-bucket elapsed) cap-reset-time)))
      (current-bucket (var-get debt-bucket))
      (new-bucket-value (min (+ current-bucket refill-amount) max-debt-bucket))
    )
    (print {
      old-debt-bucket-value: (var-get debt-bucket),
      new-debt-bucket-value: new-bucket-value,
      sender: contract-caller,
      action: "sync-debt-bucket"
    })
    (var-set debt-bucket new-bucket-value)
    (var-set last-debt-bucket-update time-now)
    SUCCESS
  )
)

(define-private (sync-collateral-bucket (collateral <token-trait>))
  (let
    (
      (cap-reset-time (var-get time-window))
      (collateral-token (contract-of collateral))
      (time-now (unwrap-panic (get-stacks-block-info? time (- stacks-block-height u1))))
      (last-ts (default-to u0 (map-get? last-collateral-bucket-update collateral-token)))
      (elapsed (if (is-eq last-ts u0) cap-reset-time (- time-now last-ts)))
      (total-liquidity (unwrap! (contract-call? collateral get-balance .state-v1) ERR-FAILED-TO-GET-BALANCE))
      (max-collateral-bucket (/ (* total-liquidity (default-to u0 (map-get? collateral-cap-factor collateral-token))) SCALING-FACTOR))
      (refill-amount (if (>= elapsed cap-reset-time) 
                     max-collateral-bucket
                     (/ (* max-collateral-bucket elapsed) cap-reset-time)))
      (current-bucket (default-to u0 (map-get? collateral-bucket collateral-token)))
      (new-bucket-value (min (+ current-bucket refill-amount) max-collateral-bucket))
    )
    (print {
      old-collateral-bucket-value: (default-to u0 (map-get? collateral-bucket collateral-token)),
      new-collateral-bucket-value: new-bucket-value,
      sender: contract-caller,
      action: "sync-collateral-bucket"
    })
    (map-set collateral-bucket collateral-token new-bucket-value)
    (map-set last-collateral-bucket-update collateral-token time-now)
    SUCCESS
  )
)

(define-private (create-proposal (proposal-id (buff 32)) (action uint) (expires-in uint))
  (begin
    (try! (is-governance-member contract-caller))
    (asserts! (not (is-some (map-get? governance-proposal proposal-id))) ERR-PROPOSAL-ALREADY-EXISTS)
    (var-set next-proposal-nonce (+ (var-get next-proposal-nonce) u1))
    (map-set governance-proposal proposal-id {
      action: action,
      approve-count: u1,
      deny-count: u0,
      completed: false,
      expires-at: (+ stacks-block-height expires-in)
    })
    (map-set proposal-approved-members {proposal-id: proposal-id, member: contract-caller} true)
    (print {
      action: "proposal-initiated",
      proposal-id: proposal-id
    })
    (print {
      action: "proposal-voted-approved",
      voter: contract-caller,
      proposal-id: proposal-id
    })
    (ok proposal-id)
))

(define-private (has-submitted-vote (proposal-id (buff 32)))
  (or 
    (default-to false (map-get? proposal-approved-members {proposal-id: proposal-id, member: contract-caller}))
    (default-to false (map-get? proposal-denied-members {proposal-id: proposal-id, member: contract-caller}))
))

(define-private (execute-set-lp-cap (proposal-id (buff 32)))
  (let ((data (unwrap-panic (map-get? lp-cap-data proposal-id))))
    (print {
      action: "execute-set-lp-cap",
      proposal-id: proposal-id,
      old-value: (var-get lp-cap-factor),
      new-value: data
    })
    (var-set lp-cap-factor data)
    SUCCESS
  )
)

(define-private (execute-set-debt-cap (proposal-id (buff 32)))
  (let ((data (unwrap-panic (map-get? debt-cap-data proposal-id))))
    (print {
      action: "execute-set-debt-cap",
      proposal-id: proposal-id,
      old-value: (var-get lp-cap-factor),
      new-value: data
    })
    (var-set debt-cap-factor data)
    SUCCESS
  )
)

(define-private (execute-set-collateral-cap (proposal-id (buff 32)))
  (let (
      (data (unwrap-panic (map-get? collateral-cap-data proposal-id)))
      (collateral (get collateral data))
      (factor (get factor data))
    )
    (print {
      action: "execute-set-collateral-cap",
      proposal-id: proposal-id,
      collateral: collateral,
      old-value: (default-to u0 (map-get? collateral-cap-factor collateral)),
      new-value: factor
    })
    (map-set collateral-cap-factor collateral factor)
    SUCCESS
  )
)

(define-private (execute-set-time-window (proposal-id (buff 32)))
  (let (
      (factor (unwrap-panic (map-get? time-window-data proposal-id)))
    )
    (print {
      action: "execute-set-time-window",
      proposal-id: proposal-id,
      old-value: (var-get time-window),
      new-value: factor
    })
    (var-set time-window factor)
    SUCCESS
  )
)

(define-private (approve-threshold-met (proposal-id (buff 32)) (action uint))
  (let (
      (proposal (unwrap! (map-get? governance-proposal proposal-id) ERR-UNKNOWN-PROPOSAL))
      (approve-count (get approve-count proposal))
      (total-count (contract-call? .meta-governance-v1 governance-multisig-count))
      (percentage (/ (* approve-count u100) total-count))
    )
    (ok (>= percentage THRESHOLD))
))

(define-private (deny-threshold-met (proposal-id (buff 32)) (action uint))
  (let (
      (proposal (unwrap! (map-get? governance-proposal proposal-id) ERR-UNKNOWN-PROPOSAL))
      (deny-count (get deny-count proposal))
      (total-count (contract-call? .meta-governance-v1 governance-multisig-count))
      (percentage (/ (* deny-count u100) total-count))
    )
    (ok (>= percentage THRESHOLD))
))

(define-private (execute-proposal (proposal-id (buff 32)) (action uint))
  (begin
    (asserts! (not (is-eq action ACTION_SET_LP_CAP)) (execute-set-lp-cap proposal-id))
    (asserts! (not (is-eq action ACTION_SET_DEBT_CAP)) (execute-set-debt-cap proposal-id))
    (asserts! (not (is-eq action ACTION_SET_COLLATERAL_CAP)) (execute-set-collateral-cap proposal-id))
    (asserts! (not (is-eq action ACTION_SET_TIME_WINDOW)) (execute-set-time-window proposal-id))
    ERR-INVALID-ACTION
))

(define-private (execute-if-approve-threshold-met (proposal-id (buff 32)))
  (let (
      (proposal (unwrap! (map-get? governance-proposal proposal-id) ERR-UNKNOWN-PROPOSAL))
      (action (get action proposal))
      (threshold (try! (approve-threshold-met proposal-id action)))
    )
    (if threshold
      (begin
        (try! (execute-proposal proposal-id action))
        (map-set governance-proposal proposal-id {
          action: (get action proposal),
          approve-count: (get approve-count proposal),
          deny-count: (get deny-count proposal),
          expires-at: (get expires-at proposal),
          completed: true
        })
        (print {
          action: "proposal-executed",
          proposal-id: proposal-id
        })
        SUCCESS
      )
      SUCCESS
    )
))

(define-private (deny-proposal-if-deny-threshold-met (proposal-id (buff 32)))
  (let (
      (proposal (unwrap! (map-get? governance-proposal proposal-id) ERR-UNKNOWN-PROPOSAL))
      (action (get action proposal))
      (threshold (try! (deny-threshold-met proposal-id action)))
    )
    (if threshold
      (begin
        (map-set governance-proposal proposal-id {
          action: (get action proposal),
          approve-count: (get approve-count proposal),
          deny-count: (get deny-count proposal),
          expires-at: (get expires-at proposal),
          completed: true
        })
        (print {
          action: "proposal-denied",
          proposal-id: proposal-id
        })
        SUCCESS
      )
      SUCCESS
    )
))

;; PUBLIC FUNCTIONS
(define-public (check-daily-lp-cap (amount uint))
  (begin 
    (asserts! (is-eq contract-caller LP-CONTRACT) ERR-RESTRICTED)
    (if (is-eq (var-get lp-cap-factor) u0)
      SUCCESS
      (begin
        (try! (sync-lp-bucket))
        (asserts! (<= amount (var-get lp-bucket)) ERR-DAILY-LP-CAP-EXCEEDED)
        (var-set lp-bucket (- (var-get lp-bucket) amount))
        SUCCESS
      )
    )
  )
)

(define-public (check-daily-debt-cap (amount uint))
  (begin 
    (asserts! (is-eq contract-caller BORROWER-CONTRACT) ERR-RESTRICTED)
    (if (is-eq (var-get debt-cap-factor) u0)
      SUCCESS
      (begin
        (unwrap-panic (sync-debt-bucket))
        (asserts! (<= amount (var-get debt-bucket)) ERR-DAILY-DEBT-CAP-EXCEEDED)
        (var-set debt-bucket (- (var-get debt-bucket) amount))
        SUCCESS
      )
    )
  )
)

(define-public (check-daily-collateral-cap (collateral <token-trait>) (amount uint))
  (let
    (
      (collateral-token (contract-of collateral))
    )
    (asserts! (is-eq contract-caller BORROWER-CONTRACT) ERR-RESTRICTED)
    (if (is-eq (default-to u0 (map-get? collateral-cap-factor collateral-token)) u0) 
      SUCCESS
      (begin 
        (try! (sync-collateral-bucket collateral))
        (asserts! (<= amount (default-to u0 (map-get? collateral-bucket collateral-token))) ERR-DAILY-COLLATERAL-CAP-EXCEEDED)
        (map-set collateral-bucket collateral-token (- (default-to u0 (map-get? collateral-bucket collateral-token)) amount))
        SUCCESS
      )
    )
  )
)

(define-public (initiate-proposal-to-update-param (action uint) (data uint) (collateral (optional <token-trait>)) (expires-in uint))
  (let (
      (proposal-nonce (var-get next-proposal-nonce))
      (proposal-id (keccak256 (unwrap! (to-consensus-buff? {
        sender: contract-caller,
        nonce: proposal-nonce,
        action: action,
        factor: data,
        expires-in: expires-in
      }) ERR-FAILED-TO-GENERATE-PROPOSAL-ID)))
      (token (match collateral tkn (some (contract-of tkn)) none))
    )
    (asserts! (or 
      (is-eq action ACTION_SET_DEBT_CAP)
      (is-eq action ACTION_SET_LP_CAP)
      (is-eq action ACTION_SET_COLLATERAL_CAP)
      (is-eq action ACTION_SET_TIME_WINDOW)
    ) ERR-INVALID-ACTION)
    (try! (create-proposal proposal-id action expires-in))
    (if (is-eq action ACTION_SET_TIME_WINDOW)
      (map-set time-window-data proposal-id data)
      (if (is-eq action ACTION_SET_DEBT_CAP)
        (map-set debt-cap-data proposal-id data)
        (if (is-eq action ACTION_SET_LP_CAP)
          (map-set lp-cap-data proposal-id data)
          (map-set collateral-cap-data proposal-id { factor: data, collateral: (unwrap-panic token) })
        )
      )
    )
    (try! (execute-if-approve-threshold-met proposal-id))
    (ok proposal-id)
))

(define-public (approve (proposal-id (buff 32)))
  (let ((proposal (unwrap! (map-get? governance-proposal proposal-id) ERR-UNKNOWN-PROPOSAL)))
    (try! (is-governance-member contract-caller))
    (asserts! (not (get completed proposal)) ERR-PROPOSAL-COMPLETED)
    (asserts! (< stacks-block-height (get expires-at proposal)) ERR-PROPOSAL-EXPIRED)
    (asserts! (not (has-submitted-vote proposal-id)) ERR-SUBMITTED-VOTE)
    (map-set proposal-approved-members {proposal-id: proposal-id, member: contract-caller} true)
    (map-set governance-proposal proposal-id {
      action: (get action proposal),
      approve-count: (+ (get approve-count proposal) u1),
      deny-count: (get deny-count proposal),
      expires-at: (get expires-at proposal),
      completed: (get completed proposal)
    })

    (print {
      action: "proposal-voted-approved",
      voter: contract-caller,
      proposal-id: proposal-id
    })
    ;; try to execute the proposal if threshold is met
    (try! (execute-if-approve-threshold-met proposal-id))
    SUCCESS
))

(define-public (deny (proposal-id (buff 32)))
  (let ((proposal (unwrap! (map-get? governance-proposal proposal-id) ERR-UNKNOWN-PROPOSAL)))
    (try! (is-governance-member contract-caller))
    (asserts! (not (get completed proposal)) ERR-PROPOSAL-COMPLETED)
    (asserts! (< stacks-block-height (get expires-at proposal)) ERR-PROPOSAL-EXPIRED)
    (asserts! (not (has-submitted-vote proposal-id)) ERR-SUBMITTED-VOTE)
    (map-set proposal-denied-members {proposal-id: proposal-id, member: contract-caller} true)
    (map-set governance-proposal proposal-id {
      action: (get action proposal),
      approve-count: (get approve-count proposal),
      deny-count: (+ (get deny-count proposal) u1),
      expires-at: (get expires-at proposal),
      completed: (get completed proposal)
    })

    (print {
      action: "proposal-voted-denied",
      voter: contract-caller,
      proposal-id: proposal-id
    })
    ;; deny proposal if the deny threshold is met
    (try! (deny-proposal-if-deny-threshold-met proposal-id))
    SUCCESS
))

;; Close the proposal when
;; - Every one voted
;; - But all the votes does not meet neither approve or deny threshold. Proposal is locked.
;; or proposal is expired
(define-public (close (proposal-id (buff 32)))
  (let (
      (proposal (unwrap! (map-get? governance-proposal proposal-id) ERR-UNKNOWN-PROPOSAL))
      (total-voted (+ (get approve-count proposal) (get deny-count proposal)))
      (total-count (contract-call? .meta-governance-v1 governance-multisig-count))
      (action (get action proposal))
      (deny-threshold (try! (deny-threshold-met proposal-id action)))
      (approve-threshold (try! (approve-threshold-met proposal-id action)))
      (has-threshold-met (or deny-threshold approve-threshold))
    )
    (try! (is-governance-member contract-caller))
    (asserts! (not (get completed proposal)) ERR-PROPOSAL-COMPLETED)
    (if (>= stacks-block-height (get expires-at proposal))
      (begin 
        (print {
          action: "proposal-expired",
          proposal-id: proposal-id
        })
        true
      )
      (begin
        (asserts! (is-eq total-count total-voted) ERR-PROPOSAL-VOTING-INCOMPLETE)
        (asserts! (not has-threshold-met) ERR-PROPOSAL-CANNOT-CLOSE)
      )
    )
    (map-set governance-proposal proposal-id {
      action: action,
      approve-count: (get approve-count proposal),
      deny-count: (get deny-count proposal),
      expires-at: (get expires-at proposal),
      completed: true
    })
    (print {
      action: "proposal-closed",
      proposal-id: proposal-id
    })
    SUCCESS
))

Functions (34)

FunctionAccessArgs
get-time-windowread-only
get-lp-cap-factorread-only
get-last-lp-bucket-updateread-only
get-lp-bucketread-only
get-debt-cap-factorread-only
get-last-debt-bucket-updateread-only
get-debt-bucketread-only
get-collateral-cap-factorread-onlycollateral: <token-trait>
get-last-collateral-bucket-updateread-onlycollateral: <token-trait>
get-collateral-bucketread-onlycollateral: <token-trait>
minread-onlya: uint, b: uint
is-governance-memberread-onlymember: principal
get-proposalread-onlyproposal-id: (buff 32
sync-lp-bucketprivate
sync-debt-bucketprivate
sync-collateral-bucketprivatecollateral: <token-trait>
create-proposalprivateproposal-id: (buff 32
has-submitted-voteprivateproposal-id: (buff 32
execute-set-lp-capprivateproposal-id: (buff 32
execute-set-debt-capprivateproposal-id: (buff 32
execute-set-collateral-capprivateproposal-id: (buff 32
execute-set-time-windowprivateproposal-id: (buff 32
approve-threshold-metprivateproposal-id: (buff 32
deny-threshold-metprivateproposal-id: (buff 32
execute-proposalprivateproposal-id: (buff 32
execute-if-approve-threshold-metprivateproposal-id: (buff 32
deny-proposal-if-deny-threshold-metprivateproposal-id: (buff 32
check-daily-lp-cappublicamount: uint
check-daily-debt-cappublicamount: uint
check-daily-collateral-cappubliccollateral: <token-trait>, amount: uint
initiate-proposal-to-update-parampublicaction: uint, data: uint, collateral: (optional <token-trait>
approvepublicproposal-id: (buff 32
denypublicproposal-id: (buff 32
closepublicproposal-id: (buff 32