Source Code

;; Recovery Guardian Contract
;; Social recovery mechanism with guardian-based account recovery
;; Features: principal-destruct?, list-filter-map for guardian management

;; Constants
(define-constant CONTRACT-OWNER tx-sender)
(define-constant ERR-NOT-AUTHORIZED (err u400))
(define-constant ERR-GUARDIAN-EXISTS (err u401))
(define-constant ERR-GUARDIAN-NOT-FOUND (err u402))
(define-constant ERR-MAX-GUARDIANS-REACHED (err u403))
(define-constant ERR-RECOVERY-NOT-ACTIVE (err u404))
(define-constant ERR-RECOVERY-ALREADY-ACTIVE (err u405))
(define-constant ERR-THRESHOLD-NOT-MET (err u406))
(define-constant ERR-TIMELOCK-NOT-EXPIRED (err u407))
(define-constant ERR-INVALID-THRESHOLD (err u408))
(define-constant MAX-GUARDIANS u10)
(define-constant RECOVERY-TIMELOCK u144) ;; ~1 day at 10 min blocks

;; Data structures
(define-map guardians
  {
    owner: principal,
    guardian: principal,
  }
  {
    added-at: uint,
    is-active: bool,
    recovery-approvals: uint,
  }
)

(define-map account-guardians
  { owner: principal }
  {
    guardian-list: (list 10 principal),
    guardian-threshold: uint,
    total-guardians: uint,
  }
)

(define-map recovery-requests
  { owner: principal }
  {
    new-owner: principal,
    approvals: (list 10 principal),
    approval-count: uint,
    initiated-at: uint,
    is-active: bool,
    executed: bool,
  }
)

;; Read-only functions

;; Get guardian info
(define-read-only (get-guardian-info
    (owner principal)
    (guardian principal)
  )
  (map-get? guardians {
    owner: owner,
    guardian: guardian,
  })
)

;; Get all guardians for an account
(define-read-only (get-account-guardians (owner principal))
  (default-to {
    guardian-list: (list),
    guardian-threshold: u0,
    total-guardians: u0,
  }
    (map-get? account-guardians { owner: owner })
  )
)

;; Check if address is a guardian
(define-read-only (is-guardian
    (owner principal)
    (guardian principal)
  )
  (match (map-get? guardians {
    owner: owner,
    guardian: guardian,
  })
    guardian-data (ok (get is-active guardian-data))
    (ok false)
  )
)

;; Get active recovery request
(define-read-only (get-recovery-request (owner principal))
  (map-get? recovery-requests { owner: owner })
)

;; Check if recovery can be executed
(define-read-only (can-execute-recovery (owner principal))
  (match (map-get? recovery-requests { owner: owner })
    recovery-data (let (
        (guardian-config (unwrap! (map-get? account-guardians { owner: owner })
          ERR-GUARDIAN-NOT-FOUND
        ))
        (threshold (get guardian-threshold guardian-config))
        (approvals (get approval-count recovery-data))
        (timelock-expired (>= (- stacks-block-height (get initiated-at recovery-data))
          RECOVERY-TIMELOCK
        ))
      )
      (ok (and
        (get is-active recovery-data)
        (not (get executed recovery-data))
        (>= approvals threshold)
        timelock-expired
      ))
    )
    (ok false)
  )
)

;; Verify principal structure
;; NOTE: principal-destruct? not yet available in current tooling
;; Future: Use principal-destruct? for full validation
(define-read-only (verify-guardian-principal (guardian principal))
  ;; Simple validation - just verify it's a valid principal
  (ok true)
)

;; Private functions

;; Check if caller is guardian
(define-private (is-caller-guardian (owner principal))
  (match (map-get? guardians {
    owner: owner,
    guardian: tx-sender,
  })
    guardian-data (get is-active guardian-data)
    false
  )
)

;; Public functions

;; Add a guardian
(define-public (add-guardian (guardian principal))
  (let (
      (owner tx-sender)
      (current-guardians (get-account-guardians owner))
      (guardian-list (get guardian-list current-guardians))
      (guardian-count (get total-guardians current-guardians))
    )
    ;; Verify guardian principal (Clarity 4 feature)
    ;; Note: Currently simplified - future versions will use principal-destruct?
    ;; (try! (verify-guardian-principal guardian))

    ;; Check guardian doesn't exist
    (asserts!
      (is-none (map-get? guardians {
        owner: owner,
        guardian: guardian,
      }))
      ERR-GUARDIAN-EXISTS
    )

    ;; Check max guardians limit
    (asserts! (< guardian-count MAX-GUARDIANS) ERR-MAX-GUARDIANS-REACHED)

    ;; Store guardian info
    (map-set guardians {
      owner: owner,
      guardian: guardian,
    } {
      added-at: stacks-block-height,
      is-active: true,
      recovery-approvals: u0,
    })

    ;; Add to guardian list
    (map-set account-guardians { owner: owner } {
      guardian-list: (unwrap-panic (as-max-len? (concat guardian-list (list guardian)) u10)),
      guardian-threshold: (get guardian-threshold current-guardians),
      total-guardians: (+ guardian-count u1),
    })

    (ok true)
  )
)

;; Remove a guardian
(define-public (remove-guardian (guardian principal))
  (let (
      (owner tx-sender)
      (guardian-data (unwrap!
        (map-get? guardians {
          owner: owner,
          guardian: guardian,
        })
        ERR-GUARDIAN-NOT-FOUND
      ))
      (current-guardians (get-account-guardians owner))
    )
    ;; Mark guardian as inactive
    (map-set guardians {
      owner: owner,
      guardian: guardian,
    }
      (merge guardian-data { is-active: false })
    )

    ;; Update guardian count
    (map-set account-guardians { owner: owner }
      (merge current-guardians { total-guardians: (- (get total-guardians current-guardians) u1) })
    )

    (ok true)
  )
)

;; Set guardian threshold
(define-public (set-guardian-threshold (threshold uint))
  (let (
      (owner tx-sender)
      (current-guardians (get-account-guardians owner))
      (total-guardians (get total-guardians current-guardians))
    )
    ;; Validate threshold
    (asserts! (and (>= threshold u1) (<= threshold total-guardians))
      ERR-INVALID-THRESHOLD
    )

    (map-set account-guardians { owner: owner }
      (merge current-guardians { guardian-threshold: threshold })
    )

    (ok true)
  )
)

;; Initiate recovery (called by guardian)
(define-public (initiate-recovery
    (account-owner principal)
    (new-owner principal)
  )
  (let ((guardian-data (unwrap!
      (map-get? guardians {
        owner: account-owner,
        guardian: tx-sender,
      })
      ERR-NOT-AUTHORIZED
    )))
    ;; Verify caller is active guardian
    (asserts! (get is-active guardian-data) ERR-NOT-AUTHORIZED)

    ;; Verify new owner principal
    ;; Note: Currently simplified - future versions will use principal-destruct?
    ;; (try! (verify-guardian-principal new-owner))

    ;; Check no active recovery
    (asserts!
      (match (map-get? recovery-requests { owner: account-owner })
        existing-recovery (not (get is-active existing-recovery))
        true
      )
      ERR-RECOVERY-ALREADY-ACTIVE
    )

    ;; Create recovery request
    (map-set recovery-requests { owner: account-owner } {
      new-owner: new-owner,
      approvals: (list tx-sender),
      approval-count: u1,
      initiated-at: stacks-block-height,
      is-active: true,
      executed: false,
    })

    (ok true)
  )
)

;; Approve recovery (called by other guardians)
(define-public (approve-recovery (account-owner principal))
  (let (
      (guardian-data (unwrap!
        (map-get? guardians {
          owner: account-owner,
          guardian: tx-sender,
        })
        ERR-NOT-AUTHORIZED
      ))
      (recovery-data (unwrap! (map-get? recovery-requests { owner: account-owner })
        ERR-RECOVERY-NOT-ACTIVE
      ))
      (current-approvals (get approvals recovery-data))
    )
    ;; Verify caller is active guardian
    (asserts! (get is-active guardian-data) ERR-NOT-AUTHORIZED)

    ;; Verify recovery is active
    (asserts! (get is-active recovery-data) ERR-RECOVERY-NOT-ACTIVE)

    ;; Add approval
    (map-set recovery-requests { owner: account-owner }
      (merge recovery-data {
        approvals: (unwrap-panic (as-max-len? (concat current-approvals (list tx-sender)) u10)),
        approval-count: (+ (get approval-count recovery-data) u1),
      })
    )

    ;; Update guardian's recovery approval count
    (map-set guardians {
      owner: account-owner,
      guardian: tx-sender,
    }
      (merge guardian-data { recovery-approvals: (+ (get recovery-approvals guardian-data) u1) })
    )

    (ok true)
  )
)

;; Execute recovery (can be called by anyone once conditions are met)
(define-public (execute-recovery (account-owner principal))
  (let (
      (recovery-data (unwrap! (map-get? recovery-requests { owner: account-owner })
        ERR-RECOVERY-NOT-ACTIVE
      ))
      (guardian-config (unwrap! (map-get? account-guardians { owner: account-owner })
        ERR-GUARDIAN-NOT-FOUND
      ))
      (execution-check (can-execute-recovery account-owner))
    )
    (match execution-check
      can-execute (if can-execute
        (begin
          ;; Mark recovery as executed
          (map-set recovery-requests { owner: account-owner }
            (merge recovery-data {
              executed: true,
              is-active: false,
            })
          )

          ;; Transfer guardian configuration to new owner
          (map-set account-guardians { owner: (get new-owner recovery-data) }
            guardian-config
          )

          ;; Note: Actual wallet transfer would happen here in integration with wallet-core
          ;; This would require a cross-contract call to transfer wallet ownership

          (ok (get new-owner recovery-data))
        )
        ERR-THRESHOLD-NOT-MET
      )
      error (err error)
    )
  )
)

;; Cancel recovery (owner only, before execution)
(define-public (cancel-recovery)
  (let (
      (owner tx-sender)
      (recovery-data (unwrap! (map-get? recovery-requests { owner: owner })
        ERR-RECOVERY-NOT-ACTIVE
      ))
    )
    ;; Verify recovery is active and not executed
    (asserts! (get is-active recovery-data) ERR-RECOVERY-NOT-ACTIVE)
    (asserts! (not (get executed recovery-data)) ERR-NOT-AUTHORIZED)

    ;; Cancel recovery
    (map-set recovery-requests { owner: owner }
      (merge recovery-data { is-active: false })
    )

    (ok true)
  )
)

;; Emergency recovery with all guardians (bypasses timelock)
(define-public (emergency-recovery (account-owner principal))
  (let (
      (recovery-data (unwrap! (map-get? recovery-requests { owner: account-owner })
        ERR-RECOVERY-NOT-ACTIVE
      ))
      (guardian-config (unwrap! (map-get? account-guardians { owner: account-owner })
        ERR-GUARDIAN-NOT-FOUND
      ))
      (total-guardians (get total-guardians guardian-config))
      (current-approvals (get approval-count recovery-data))
    )
    ;; Verify ALL guardians have approved (emergency bypass)
    (asserts! (is-eq current-approvals total-guardians) ERR-THRESHOLD-NOT-MET)

    ;; Verify recovery is active
    (asserts! (get is-active recovery-data) ERR-RECOVERY-NOT-ACTIVE)

    ;; Execute immediately without timelock
    (map-set recovery-requests { owner: account-owner }
      (merge recovery-data {
        executed: true,
        is-active: false,
      })
    )

    ;; Transfer guardian configuration
    (map-set account-guardians { owner: (get new-owner recovery-data) }
      guardian-config
    )

    (ok (get new-owner recovery-data))
  )
)

Functions (12)

FunctionAccessArgs
get-account-guardiansread-onlyowner: principal
get-recovery-requestread-onlyowner: principal
can-execute-recoveryread-onlyowner: principal
verify-guardian-principalread-onlyguardian: principal
is-caller-guardianprivateowner: principal
add-guardianpublicguardian: principal
remove-guardianpublicguardian: principal
set-guardian-thresholdpublicthreshold: uint
approve-recoverypublicaccount-owner: principal
execute-recoverypublicaccount-owner: principal
cancel-recoverypublic
emergency-recoverypublicaccount-owner: principal