Source Code

;; bitpay-treasury
;; Treasury contract for fee collection and management

;; Constants
(define-constant CONTRACT_OWNER tx-sender)
(define-constant ERR_UNAUTHORIZED (err u500))
(define-constant ERR_INSUFFICIENT_BALANCE (err u501))
(define-constant ERR_INVALID_AMOUNT (err u502))
(define-constant ERR_PAUSED (err u503))
(define-constant ERR_PROPOSAL_NOT_FOUND (err u504))
(define-constant ERR_ALREADY_APPROVED (err u505))
(define-constant ERR_ALREADY_EXECUTED (err u506))
(define-constant ERR_INSUFFICIENT_APPROVALS (err u507))
(define-constant ERR_PROPOSAL_EXPIRED (err u508))
(define-constant ERR_TIMELOCK_NOT_ELAPSED (err u509))
(define-constant ERR_NOT_ADMIN (err u510))
(define-constant ERR_ALREADY_ADMIN (err u511))
(define-constant ERR_EXCEEDS_DAILY_LIMIT (err u512))

;; Fee percentage (basis points: 100 = 1%, 50 = 0.5%)
(define-constant DEFAULT_FEE_BPS u50) ;; 0.5% fee

;; Multi-sig configuration (3-of-5 institutional standard)
(define-constant REQUIRED_SIGNATURES u3)
(define-constant TOTAL_ADMIN_SLOTS u5)
(define-constant TIMELOCK_BLOCKS u144) ;; ~24 hours (144 blocks)
(define-constant PROPOSAL_EXPIRY_BLOCKS u1008) ;; ~7 days
(define-constant DAILY_WITHDRAWAL_LIMIT u100000000) ;; 100 sBTC per day

;; Data vars
(define-data-var treasury-balance uint u0)
(define-data-var fee-bps uint DEFAULT_FEE_BPS)
(define-data-var total-fees-collected uint u0)
(define-data-var admin principal CONTRACT_OWNER) ;; Legacy admin (will migrate to multi-sig)
(define-data-var pending-admin (optional principal) none)
(define-data-var next-proposal-id uint u0)
(define-data-var last-withdrawal-block uint u0)
(define-data-var withdrawn-today uint u0)
(define-data-var active-admin-count uint u1) ;; Start with 1 (CONTRACT_OWNER)

;; Maps
(define-map fee-recipients
    principal
    uint
)

;; Multi-sig admins (5 slots for institutional governance)
(define-map multisig-admins
    principal
    bool
)

;; Withdrawal proposals
(define-map withdrawal-proposals
    uint ;; proposal-id
    {
        proposer: principal,
        amount: uint,
        recipient: principal,
        approvals: (list 10 principal),
        executed: bool,
        proposed-at: uint,
        expires-at: uint,
        description: (string-ascii 256),
    }
)

;; Admin management proposals
(define-map admin-proposals
    uint ;; proposal-id
    {
        proposer: principal,
        action: (string-ascii 10), ;; "add" or "remove"
        target-admin: principal,
        approvals: (list 10 principal),
        executed: bool,
        proposed-at: uint,
        expires-at: uint,
    }
)

;; Initialize first admin (deployer)
(map-set multisig-admins CONTRACT_OWNER true)

;; Authorization checks

;; Check if caller is the legacy admin
;; @returns: true if caller is admin
(define-private (is-admin)
    (is-eq tx-sender (var-get admin))
)

;; Check if a user is a multisig admin
;; @param user: Principal to check
;; @returns: true if user is multisig admin
(define-private (is-multisig-admin (user principal))
    (default-to false (map-get? multisig-admins user))
)

;; Check if caller is either multisig admin or legacy admin
;; @returns: true if caller has admin privileges
(define-private (is-multisig-admin-or-legacy)
    (or (is-multisig-admin tx-sender) (is-admin))
)

;; Helper: Check if principal is in approval list
;; @param item: Principal to search for
;; @param lst: List of principals
;; @returns: true if item is in list
(define-private (is-in-list
        (item principal)
        (lst (list 10 principal))
    )
    (is-some (index-of? lst item))
)

;; Helper: Count active multisig admins
;; @returns: (ok admin-count)
(define-read-only (count-admins)
    (ok (var-get active-admin-count))
)

;; Helper: Get total available admin slots
;; @returns: (ok total-slots)
(define-read-only (get-total-admin-slots)
    (ok TOTAL_ADMIN_SLOTS)
)

;; Check if protocol is paused via access-control
;; @returns: (ok true) if not paused, error if paused
(define-private (check-not-paused)
    (let ((paused-check (contract-call? .bitpay-access-control-v4 is-paused)))
        (asserts! (not paused-check) ERR_PAUSED)
        (ok true)
    )
)

;; Calculate fee amount based on basis points
;; @param amount: Gross amount to calculate fee on
;; @returns: (ok fee-amount)
(define-read-only (calculate-fee (amount uint))
    (let ((fee (/ (* amount (var-get fee-bps)) u10000)))
        (ok fee)
    )
)

;; Collect fee from a stream (called by bitpay-core)
;; @param amount: Amount to collect as fee
;; @returns: (ok fee-amount) on success
;; #[allow(unchecked_data)]
(define-public (collect-fee (amount uint))
    (begin
        (try! (check-not-paused))
        (asserts! (> amount u0) ERR_INVALID_AMOUNT)

        (let ((fee (unwrap-panic (calculate-fee amount))))
            ;; Transfer fee from sender to treasury
            (try! (contract-call? .bitpay-sbtc-helper-v4 transfer-to-vault fee
                tx-sender
            ))

            ;; Update treasury balance
            (var-set treasury-balance (+ (var-get treasury-balance) fee))
            (var-set total-fees-collected (+ (var-get total-fees-collected) fee))

            (print {
                event: "treasury-fee-collected",
                amount: fee,
                caller: tx-sender,
                new-balance: (var-get treasury-balance),
            })

            (ok fee)
        )
    )
)

;; Collect cancellation fee from vault (called by bitpay-core after stream cancellation)
;; This transfers sBTC from the vault to treasury and updates accounting
;; @param amount: Amount of cancellation fee to collect from vault
;; @returns: (ok amount) on success
;; #[allow(unchecked_data)]
(define-public (collect-cancellation-fee (amount uint))
    (begin
        (try! (check-not-paused))
        (asserts! (> amount u0) ERR_INVALID_AMOUNT)

        ;; Only authorized contracts (bitpay-core) can collect cancellation fees
        (try! (contract-call? .bitpay-access-control-v4 assert-authorized-contract
            contract-caller
        ))

        ;; Transfer sBTC from vault to this treasury contract
        (try! (as-contract (contract-call? .bitpay-sbtc-helper-v4 transfer-from-vault amount
            tx-sender
        )))

        ;; Update treasury balance
        (var-set treasury-balance (+ (var-get treasury-balance) amount))
        (var-set total-fees-collected (+ (var-get total-fees-collected) amount))

        (print {
            event: "treasury-cancellation-fee-collected",
            amount: amount,
            caller: contract-caller,
            new-balance: (var-get treasury-balance),
        })

        (ok amount)
    )
)

;; Collect marketplace fee (called by bitpay-marketplace after NFT sale)
;; This updates treasury accounting after receiving marketplace fee payment
;; NOTE: Payment already sent via direct sBTC transfer, this just updates accounting
;; @param amount: Amount of marketplace fee received
;; @returns: (ok amount) on success
;; #[allow(unchecked_data)]
(define-public (collect-marketplace-fee (amount uint))
    (begin
        (try! (check-not-paused))
        (asserts! (> amount u0) ERR_INVALID_AMOUNT)

        ;; Only authorized contracts (bitpay-marketplace) can collect marketplace fees
        (try! (contract-call? .bitpay-access-control-v4 assert-authorized-contract
            contract-caller
        ))

        ;; Update treasury balance (sBTC already received via direct transfer)
        (var-set treasury-balance (+ (var-get treasury-balance) amount))
        (var-set total-fees-collected (+ (var-get total-fees-collected) amount))

        (print {
            event: "treasury-marketplace-fee-collected",
            amount: amount,
            caller: contract-caller,
            new-balance: (var-get treasury-balance),
        })

        (ok amount)
    )
)

;; Withdraw from treasury (admin only)
;; @param amount: Amount to withdraw in sats
;; @param recipient: Principal to receive funds
;; @returns: (ok amount) on success
;; #[allow(unchecked_data)]
(define-public (withdraw
        (amount uint)
        (recipient principal)
    )
    (begin
        (asserts! (is-admin) ERR_UNAUTHORIZED)
        (asserts! (> amount u0) ERR_INVALID_AMOUNT)
        (asserts! (<= amount (var-get treasury-balance)) ERR_INSUFFICIENT_BALANCE)

        ;; Transfer from treasury to recipient
        (try! (as-contract (contract-call? .bitpay-sbtc-helper-v4 transfer-from-vault amount
            recipient
        )))

        ;; Update treasury balance
        (var-set treasury-balance (- (var-get treasury-balance) amount))

        (print {
            event: "treasury-withdrawal",
            amount: amount,
            recipient: recipient,
            admin: tx-sender,
            new-balance: (var-get treasury-balance),
        })

        (ok amount)
    )
)

;; Distribute fees to recipients
;; @param recipient: Principal to receive distribution
;; @param amount: Amount to distribute in sats
;; @returns: (ok amount) on success
;; #[allow(unchecked_data)]
(define-public (distribute-to-recipient
        (recipient principal)
        (amount uint)
    )
    (begin
        (asserts! (is-admin) ERR_UNAUTHORIZED)
        (asserts! (> amount u0) ERR_INVALID_AMOUNT)
        (asserts! (<= amount (var-get treasury-balance)) ERR_INSUFFICIENT_BALANCE)

        ;; Transfer to recipient
        (try! (as-contract (contract-call? .bitpay-sbtc-helper-v4 transfer-from-vault amount
            recipient
        )))

        ;; Update balances
        (var-set treasury-balance (- (var-get treasury-balance) amount))
        (map-set fee-recipients recipient
            (+ (default-to u0 (map-get? fee-recipients recipient)) amount)
        )

        (print {
            event: "treasury-distribution",
            amount: amount,
            recipient: recipient,
            admin: tx-sender,
            new-balance: (var-get treasury-balance),
        })

        (ok amount)
    )
)

;; Update fee percentage (admin only)
;; @param new-fee-bps: New fee in basis points (max 1000 = 10%)
;; @returns: (ok new-fee-bps) on success
;; #[allow(unchecked_data)]
(define-public (set-fee-bps (new-fee-bps uint))
    (begin
        (asserts! (is-admin) ERR_UNAUTHORIZED)
        (asserts! (<= new-fee-bps u1000) ERR_INVALID_AMOUNT) ;; Max 10% fee

        (let ((old-fee (var-get fee-bps)))
            (var-set fee-bps new-fee-bps)

            (print {
                event: "treasury-fee-updated",
                old-fee-bps: old-fee,
                new-fee-bps: new-fee-bps,
                admin: tx-sender,
            })

            (ok new-fee-bps)
        )
    )
)

;; Propose admin transfer (step 1 of 2)
;; @param new-admin: Principal to transfer admin role to
;; @returns: (ok new-admin) on success
;; #[allow(unchecked_data)]
(define-public (propose-admin-transfer (new-admin principal))
    (begin
        (asserts! (is-admin) ERR_UNAUTHORIZED)
        (asserts! (not (is-eq new-admin (var-get admin))) ERR_INVALID_AMOUNT)

        (var-set pending-admin (some new-admin))

        (print {
            event: "treasury-admin-transfer-proposed",
            current-admin: (var-get admin),
            proposed-admin: new-admin,
        })

        (ok new-admin)
    )
)

;; Accept admin transfer (step 2 of 2)
;; @returns: (ok tx-sender) on success
;; #[allow(unchecked_data)]
(define-public (accept-admin-transfer)
    (let ((pending (var-get pending-admin)))
        (asserts! (is-some pending) ERR_UNAUTHORIZED)
        (asserts! (is-eq tx-sender (unwrap-panic pending)) ERR_UNAUTHORIZED)

        (let ((old-admin (var-get admin)))
            (var-set admin tx-sender)
            (var-set pending-admin none)

            (print {
                event: "treasury-admin-transfer-completed",
                old-admin: old-admin,
                new-admin: tx-sender,
            })

            (ok tx-sender)
        )
    )
)

;; Cancel pending admin transfer
;; @returns: (ok true) on success
;; #[allow(unchecked_data)]
(define-public (cancel-admin-transfer)
    (begin
        (asserts! (is-admin) ERR_UNAUTHORIZED)
        (asserts! (is-some (var-get pending-admin)) ERR_INVALID_AMOUNT)

        (var-set pending-admin none)

        (print {
            event: "treasury-admin-transfer-cancelled",
            cancelled-by: tx-sender,
        })

        (ok true)
    )
)

;; Read-only functions

;; Get current treasury balance
;; @returns: (ok balance)
(define-read-only (get-treasury-balance)
    (ok (var-get treasury-balance))
)

;; Get current fee in basis points
;; @returns: (ok fee-bps)
(define-read-only (get-fee-bps)
    (ok (var-get fee-bps))
)

;; Get total fees collected all-time
;; @returns: (ok total-fees)
(define-read-only (get-total-fees-collected)
    (ok (var-get total-fees-collected))
)

;; Get current admin
;; @returns: (ok admin)
(define-read-only (get-admin)
    (ok (var-get admin))
)

;; Get pending admin transfer
;; @returns: (ok optional-pending-admin)
(define-read-only (get-pending-admin)
    (ok (var-get pending-admin))
)

;; Get total distributed to a recipient
;; @param recipient: Principal to check
;; @returns: (ok amount)
(define-read-only (get-recipient-total (recipient principal))
    (ok (default-to u0 (map-get? fee-recipients recipient)))
)

;; Calculate net amount after fee deduction
;; @param gross-amount: Amount before fee
;; @returns: (ok net-amount)
(define-read-only (get-amount-after-fee (gross-amount uint))
    (let ((fee (unwrap-panic (calculate-fee gross-amount))))
        (ok (- gross-amount fee))
    )
)

;; =============================================================================
;; MULTI-SIG TREASURY FUNCTIONS (Professional Grade)
;; =============================================================================

;; ==========================================
;; WITHDRAWAL PROPOSALS (With Timelock & Limits)
;; ==========================================

;; Propose a withdrawal (requires 3-of-5 approval + 24h timelock)
;; @param amount: Amount to withdraw in sats
;; @param recipient: Principal to receive funds
;; @param description: Description of withdrawal purpose
;; @returns: (ok proposal-id) on success
;; #[allow(unchecked_data)]
(define-public (propose-multisig-withdrawal
        (amount uint)
        (recipient principal)
        (description (string-ascii 256))
    )
    (let (
            (proposal-id (var-get next-proposal-id))
            (expiry (+ stacks-block-height PROPOSAL_EXPIRY_BLOCKS))
        )
        ;; Only multisig admins can propose
        (asserts! (is-multisig-admin tx-sender) ERR_UNAUTHORIZED)
        (asserts! (> amount u0) ERR_INVALID_AMOUNT)
        (asserts! (<= amount (var-get treasury-balance)) ERR_INSUFFICIENT_BALANCE)

        ;; Create proposal
        (map-set withdrawal-proposals proposal-id {
            proposer: tx-sender,
            amount: amount,
            recipient: recipient,
            approvals: (list tx-sender), ;; Proposer auto-approves
            executed: false,
            proposed-at: stacks-block-height,
            expires-at: expiry,
            description: description,
        })

        (var-set next-proposal-id (+ proposal-id u1))

        (print {
            event: "treasury-withdrawal-proposed",
            proposal-id: proposal-id,
            amount: amount,
            recipient: recipient,
            proposer: tx-sender,
            description: description,
            expires-at: expiry,
        })

        (ok proposal-id)
    )
)

;; Approve a withdrawal proposal
;; @param proposal-id: ID of the proposal to approve
;; @returns: (ok true) on success
;; #[allow(unchecked_data)]
(define-public (approve-multisig-withdrawal (proposal-id uint))
    (let (
            (proposal (unwrap! (map-get? withdrawal-proposals proposal-id)
                ERR_PROPOSAL_NOT_FOUND
            ))
            (current-approvals (get approvals proposal))
        )
        ;; Checks
        (asserts! (is-multisig-admin tx-sender) ERR_UNAUTHORIZED)
        (asserts! (not (is-in-list tx-sender current-approvals))
            ERR_ALREADY_APPROVED
        )
        (asserts! (not (get executed proposal)) ERR_ALREADY_EXECUTED)
        (asserts! (< stacks-block-height (get expires-at proposal))
            ERR_PROPOSAL_EXPIRED
        )

        ;; Add approval
        (map-set withdrawal-proposals proposal-id
            (merge proposal { approvals: (unwrap! (as-max-len? (append current-approvals tx-sender) u10)
                ERR_INVALID_AMOUNT
            ) }
            ))

        (print {
            event: "treasury-withdrawal-approved",
            proposal-id: proposal-id,
            approver: tx-sender,
            total-approvals: (+ (len current-approvals) u1),
            required: REQUIRED_SIGNATURES,
        })

        (ok true)
    )
)

;; Execute withdrawal (once 3 approvals + timelock elapsed)
;; @param proposal-id: ID of the proposal to execute
;; @returns: (ok true) on success
;; #[allow(unchecked_data)]
(define-public (execute-multisig-withdrawal (proposal-id uint))
    (let (
            (proposal (unwrap! (map-get? withdrawal-proposals proposal-id)
                ERR_PROPOSAL_NOT_FOUND
            ))
            (approval-count (len (get approvals proposal)))
            (timelock-elapsed (+ (get proposed-at proposal) TIMELOCK_BLOCKS))
            (amount (get amount proposal))
        )
        ;; Checks
        (asserts! (not (get executed proposal)) ERR_ALREADY_EXECUTED)
        (asserts! (>= approval-count REQUIRED_SIGNATURES)
            ERR_INSUFFICIENT_APPROVALS
        )
        (asserts! (>= stacks-block-height timelock-elapsed)
            ERR_TIMELOCK_NOT_ELAPSED
        )
        (asserts! (< stacks-block-height (get expires-at proposal))
            ERR_PROPOSAL_EXPIRED
        )

        ;; Check daily limit
        (try! (check-daily-limit amount))

        ;; Execute withdrawal
        (try! (as-contract (contract-call? .bitpay-sbtc-helper-v4 transfer-from-vault amount
            (get recipient proposal)
        )))

        ;; Mark as executed
        (map-set withdrawal-proposals proposal-id
            (merge proposal { executed: true })
        )

        ;; Update treasury balance
        (var-set treasury-balance (- (var-get treasury-balance) amount))

        ;; Update daily limit tracking
        (update-daily-limit amount)

        (print {
            event: "treasury-withdrawal-executed",
            proposal-id: proposal-id,
            amount: amount,
            recipient: (get recipient proposal),
            approvals: approval-count,
        })

        (ok true)
    )
)

;; Check and update daily withdrawal limit
;; @param amount: Amount to check against daily limit
;; @returns: (ok true) if within limit
(define-private (check-daily-limit (amount uint))
    (let (
            (current-block stacks-block-height)
            (last-block (var-get last-withdrawal-block))
            (blocks-per-day u144)
        )
        ;; Reset if new day
        (if (>= (- current-block last-block) blocks-per-day)
            (var-set withdrawn-today u0)
            true
        )

        ;; Check limit
        (asserts!
            (<= (+ (var-get withdrawn-today) amount) DAILY_WITHDRAWAL_LIMIT)
            ERR_EXCEEDS_DAILY_LIMIT
        )

        (ok true)
    )
)

;; Update daily withdrawal limit tracking
;; @param amount: Amount withdrawn
;; @returns: true
(define-private (update-daily-limit (amount uint))
    (begin
        (var-set withdrawn-today (+ (var-get withdrawn-today) amount))
        (var-set last-withdrawal-block stacks-block-height)
        true
    )
)

;; ==========================================
;; ADMIN MANAGEMENT (Add/Remove via Multi-Sig)
;; ==========================================

;; Propose adding a new admin
;; @param new-admin: Principal to add as admin
;; @returns: (ok proposal-id) on success
;; #[allow(unchecked_data)]
(define-public (propose-add-admin (new-admin principal))
    (let (
            (proposal-id (var-get next-proposal-id))
            (expiry (+ stacks-block-height PROPOSAL_EXPIRY_BLOCKS))
        )
        ;; Checks
        (asserts! (is-multisig-admin tx-sender) ERR_UNAUTHORIZED)
        (asserts! (not (is-multisig-admin new-admin)) ERR_ALREADY_ADMIN)

        ;; Create proposal
        (map-set admin-proposals proposal-id {
            proposer: tx-sender,
            action: "add",
            target-admin: new-admin,
            approvals: (list tx-sender),
            executed: false,
            proposed-at: stacks-block-height,
            expires-at: expiry,
        })

        (var-set next-proposal-id (+ proposal-id u1))

        (print {
            event: "treasury-add-admin-proposed",
            proposal-id: proposal-id,
            new-admin: new-admin,
            proposer: tx-sender,
        })

        (ok proposal-id)
    )
)

;; Propose removing an admin
;; @param target-admin: Principal to remove from admin list
;; @returns: (ok proposal-id) on success
;; #[allow(unchecked_data)]
(define-public (propose-remove-admin (target-admin principal))
    (let (
            (proposal-id (var-get next-proposal-id))
            (expiry (+ stacks-block-height PROPOSAL_EXPIRY_BLOCKS))
        )
        ;; Checks
        (asserts! (is-multisig-admin tx-sender) ERR_UNAUTHORIZED)
        (asserts! (is-multisig-admin target-admin) ERR_NOT_ADMIN)
        (asserts! (not (is-eq target-admin tx-sender)) ERR_UNAUTHORIZED)
        ;; Can't remove self

        ;; Create proposal
        (map-set admin-proposals proposal-id {
            proposer: tx-sender,
            action: "remove",
            target-admin: target-admin,
            approvals: (list tx-sender),
            executed: false,
            proposed-at: stacks-block-height,
            expires-at: expiry,
        })

        (var-set next-proposal-id (+ proposal-id u1))

        (print {
            event: "treasury-remove-admin-proposed",
            proposal-id: proposal-id,
            target-admin: target-admin,
            proposer: tx-sender,
        })

        (ok proposal-id)
    )
)

;; Approve admin management proposal
;; @param proposal-id: ID of the proposal to approve
;; @returns: (ok true) on success
;; #[allow(unchecked_data)]
(define-public (approve-admin-proposal (proposal-id uint))
    (let (
            (proposal (unwrap! (map-get? admin-proposals proposal-id)
                ERR_PROPOSAL_NOT_FOUND
            ))
            (current-approvals (get approvals proposal))
        )
        ;; Checks
        (asserts! (is-multisig-admin tx-sender) ERR_UNAUTHORIZED)
        (asserts! (not (is-in-list tx-sender current-approvals))
            ERR_ALREADY_APPROVED
        )
        (asserts! (not (get executed proposal)) ERR_ALREADY_EXECUTED)
        (asserts! (< stacks-block-height (get expires-at proposal))
            ERR_PROPOSAL_EXPIRED
        )

        ;; Add approval
        (map-set admin-proposals proposal-id
            (merge proposal { approvals: (unwrap! (as-max-len? (append current-approvals tx-sender) u10)
                ERR_INVALID_AMOUNT
            ) }
            ))

        (print {
            event: "treasury-admin-proposal-approved",
            proposal-id: proposal-id,
            approver: tx-sender,
            total-approvals: (+ (len current-approvals) u1),
        })

        (ok true)
    )
)

;; Execute admin management proposal
;; @param proposal-id: ID of the proposal to execute
;; @returns: (ok true) on success
;; #[allow(unchecked_data)]
(define-public (execute-admin-proposal (proposal-id uint))
    (let (
            (proposal (unwrap! (map-get? admin-proposals proposal-id)
                ERR_PROPOSAL_NOT_FOUND
            ))
            (approval-count (len (get approvals proposal)))
            (action (get action proposal))
            (target (get target-admin proposal))
        )
        ;; Checks
        (asserts! (not (get executed proposal)) ERR_ALREADY_EXECUTED)
        (asserts! (>= approval-count REQUIRED_SIGNATURES)
            ERR_INSUFFICIENT_APPROVALS
        )
        (asserts! (< stacks-block-height (get expires-at proposal))
            ERR_PROPOSAL_EXPIRED
        )

        ;; Execute action and update counter
        (if (is-eq action "add")
            (begin
                (map-set multisig-admins target true)
                (var-set active-admin-count (+ (var-get active-admin-count) u1))
            )
            (begin
                (map-delete multisig-admins target)
                (var-set active-admin-count (- (var-get active-admin-count) u1))
            )
        )

        ;; Mark as executed
        (map-set admin-proposals proposal-id (merge proposal { executed: true }))

        (print {
            event: "treasury-admin-proposal-executed",
            proposal-id: proposal-id,
            action: action,
            target-admin: target,
            approvals: approval-count,
        })

        (ok true)
    )
)

;; ==========================================
;; READ-ONLY FUNCTIONS (Query Multi-Sig State)
;; ==========================================

;; Get withdrawal proposal details
;; @param proposal-id: ID of the proposal
;; @returns: (ok optional-proposal)
(define-read-only (get-withdrawal-proposal (proposal-id uint))
    (ok (map-get? withdrawal-proposals proposal-id))
)

;; Get admin management proposal details
;; @param proposal-id: ID of the proposal
;; @returns: (ok optional-proposal)
(define-read-only (get-admin-proposal (proposal-id uint))
    (ok (map-get? admin-proposals proposal-id))
)

;; Check if a user is a multisig admin
;; @param user: Principal to check
;; @returns: (ok is-admin)
(define-read-only (is-multisig-admin-check (user principal))
    (ok (is-multisig-admin user))
)

;; Get multisig configuration parameters
;; @returns: (ok config)
(define-read-only (get-multisig-config)
    (ok {
        required-signatures: REQUIRED_SIGNATURES,
        total-slots: TOTAL_ADMIN_SLOTS,
        timelock-blocks: TIMELOCK_BLOCKS,
        proposal-expiry-blocks: PROPOSAL_EXPIRY_BLOCKS,
        daily-limit: DAILY_WITHDRAWAL_LIMIT,
        withdrawn-today: (var-get withdrawn-today),
        last-withdrawal-block: (var-get last-withdrawal-block),
    })
)

;; Get next proposal ID
;; @returns: (ok next-id)
(define-read-only (get-next-proposal-id)
    (ok (var-get next-proposal-id))
)

;; Get this contract's address (for receiving payments)
;; @returns: (ok contract-address)
(define-read-only (get-contract-address)
    (ok (as-contract tx-sender))
)

Functions (37)

FunctionAccessArgs
is-adminprivate
is-multisig-adminprivateuser: principal
is-multisig-admin-or-legacyprivate
is-in-listprivateitem: principal, lst: (list 10 principal
count-adminsread-only
get-total-admin-slotsread-only
check-not-pausedprivate
calculate-feeread-onlyamount: uint
collect-feepublicamount: uint
collect-cancellation-feepublicamount: uint
collect-marketplace-feepublicamount: uint
set-fee-bpspublicnew-fee-bps: uint
propose-admin-transferpublicnew-admin: principal
accept-admin-transferpublic
cancel-admin-transferpublic
get-treasury-balanceread-only
get-fee-bpsread-only
get-total-fees-collectedread-only
get-adminread-only
get-pending-adminread-only
get-recipient-totalread-onlyrecipient: principal
get-amount-after-feeread-onlygross-amount: uint
propose-multisig-withdrawalpublicamount: uint, recipient: principal, description: (string-ascii 256
approve-multisig-withdrawalpublicproposal-id: uint
execute-multisig-withdrawalpublicproposal-id: uint
check-daily-limitprivateamount: uint
update-daily-limitprivateamount: uint
propose-add-adminpublicnew-admin: principal
propose-remove-adminpublictarget-admin: principal
approve-admin-proposalpublicproposal-id: uint
execute-admin-proposalpublicproposal-id: uint
get-withdrawal-proposalread-onlyproposal-id: uint
get-admin-proposalread-onlyproposal-id: uint
is-multisig-admin-checkread-onlyuser: principal
get-multisig-configread-only
get-next-proposal-idread-only
get-contract-addressread-only