Source Code

;; Simple Multi-Sig Vault - Working Version
;; Secure vaults with multiple signers

;; Error codes
(define-constant ERR-NOT-AUTHORIZED (err u300))
(define-constant ERR-INVALID-AMOUNT (err u301))
(define-constant ERR-VAULT-NOT-FOUND (err u302))
(define-constant ERR-ALREADY-SIGNED (err u303))
(define-constant ERR-NOT-ENOUGH-SIGNATURES (err u304))
(define-constant ERR-PROPOSAL-NOT-FOUND (err u305))
(define-constant ERR-INVALID-SIGNERS (err u306))

;; Data variables
(define-data-var vault-counter uint u0)
(define-data-var proposal-counter uint u0)

;; Vault structure
(define-map vaults
  uint
  {
    owner: principal,
    balance: uint,
    required-sigs: uint,
    active: bool
  }
)

;; Vault signers (stored separately for iterations)
(define-map vault-signers
  { vault-id: uint, signer: principal }
  { authorized: bool }
)

;; Withdrawal proposals
(define-map proposals
  uint
  {
    vault-id: uint,
    proposer: principal,
    recipient: principal,
    amount: uint,
    signatures: uint,
    executed: bool,
    created-at: uint
  }
)

;; Proposal signatures
(define-map proposal-signatures
  { proposal-id: uint, signer: principal }
  { signed: bool }
)

;; Read-only functions
(define-read-only (get-vault (vault-id uint))
  (map-get? vaults vault-id)
)

(define-read-only (get-proposal (proposal-id uint))
  (map-get? proposals proposal-id)
)

(define-read-only (is-signer (vault-id uint) (signer principal))
  (default-to false 
    (get authorized (map-get? vault-signers { vault-id: vault-id, signer: signer }))
  )
)

(define-read-only (has-signed (proposal-id uint) (signer principal))
  (default-to false
    (get signed (map-get? proposal-signatures { proposal-id: proposal-id, signer: signer }))
  )
)

;; Public functions
(define-public (create-vault (signer1 principal) (signer2 principal) (required-sigs uint))
  (let
    (
      (vault-id (+ (var-get vault-counter) u1))
    )
    (asserts! (> required-sigs u0) ERR-INVALID-SIGNERS)
    (asserts! (<= required-sigs u2) ERR-INVALID-SIGNERS)
    
    ;; Create vault
    (map-set vaults vault-id {
      owner: tx-sender,
      balance: u0,
      required-sigs: required-sigs,
      active: true
    })
    
    ;; Add signers
    (map-set vault-signers { vault-id: vault-id, signer: tx-sender } { authorized: true })
    (map-set vault-signers { vault-id: vault-id, signer: signer1 } { authorized: true })
    (map-set vault-signers { vault-id: vault-id, signer: signer2 } { authorized: true })
    
    (var-set vault-counter vault-id)
    (ok vault-id)
  )
)

(define-public (deposit (vault-id uint) (amount uint))
  (let
    (
      (vault (unwrap! (get-vault vault-id) ERR-VAULT-NOT-FOUND))
    )
    (asserts! (> amount u0) ERR-INVALID-AMOUNT)
    (asserts! (get active vault) ERR-NOT-AUTHORIZED)
    
    ;; Transfer STX to contract
    (try! (stx-transfer? amount tx-sender (as-contract tx-sender)))
    
    ;; Update vault balance
    (map-set vaults vault-id (merge vault {
      balance: (+ (get balance vault) amount)
    }))
    
    (ok true)
  )
)

(define-public (propose-withdrawal (vault-id uint) (recipient principal) (amount uint))
  (let
    (
      (vault (unwrap! (get-vault vault-id) ERR-VAULT-NOT-FOUND))
      (proposal-id (+ (var-get proposal-counter) u1))
    )
    (asserts! (is-signer vault-id tx-sender) ERR-NOT-AUTHORIZED)
    (asserts! (> amount u0) ERR-INVALID-AMOUNT)
    (asserts! (<= amount (get balance vault)) ERR-INVALID-AMOUNT)
    
    ;; Create proposal
    (map-set proposals proposal-id {
      vault-id: vault-id,
      proposer: tx-sender,
      recipient: recipient,
      amount: amount,
      signatures: u1,
      executed: false,
      created-at: stacks-block-height
    })
    
    ;; Auto-sign by proposer
    (map-set proposal-signatures { proposal-id: proposal-id, signer: tx-sender } { signed: true })
    
    (var-set proposal-counter proposal-id)
    (ok proposal-id)
  )
)

(define-public (sign-proposal (proposal-id uint))
  (let
    (
      (proposal (unwrap! (get-proposal proposal-id) ERR-PROPOSAL-NOT-FOUND))
      (vault-id (get vault-id proposal))
      (vault (unwrap! (get-vault vault-id) ERR-VAULT-NOT-FOUND))
    )
    (asserts! (is-signer vault-id tx-sender) ERR-NOT-AUTHORIZED)
    (asserts! (not (has-signed proposal-id tx-sender)) ERR-ALREADY-SIGNED)
    (asserts! (not (get executed proposal)) ERR-PROPOSAL-NOT-FOUND)
    
    ;; Add signature
    (map-set proposal-signatures { proposal-id: proposal-id, signer: tx-sender } { signed: true })
    
    (let
      (
        (new-sig-count (+ (get signatures proposal) u1))
        (updated-proposal (merge proposal { signatures: new-sig-count }))
      )
      (map-set proposals proposal-id updated-proposal)
      
      ;; Execute if enough signatures
      (if (>= new-sig-count (get required-sigs vault))
        (execute-withdrawal proposal-id)
        (ok true)
      )
    )
  )
)

(define-private (execute-withdrawal (proposal-id uint))
  (let
    (
      (proposal (unwrap! (get-proposal proposal-id) ERR-PROPOSAL-NOT-FOUND))
      (vault-id (get vault-id proposal))
      (vault (unwrap! (get-vault vault-id) ERR-VAULT-NOT-FOUND))
      (amount (get amount proposal))
      (recipient (get recipient proposal))
    )
    ;; Transfer STX from contract
    (try! (as-contract (stx-transfer? amount tx-sender recipient)))
    
    ;; Update vault balance
    (map-set vaults vault-id (merge vault {
      balance: (- (get balance vault) amount)
    }))
    
    ;; Mark proposal as executed
    (map-set proposals proposal-id (merge proposal { executed: true }))
    
    (ok true)
  )
)

;; Initialize
(begin
  (var-set vault-counter u0)
  (var-set proposal-counter u0)
)

Functions (9)

FunctionAccessArgs
get-vaultread-onlyvault-id: uint
get-proposalread-onlyproposal-id: uint
is-signerread-onlyvault-id: uint, signer: principal
has-signedread-onlyproposal-id: uint, signer: principal
create-vaultpublicsigner1: principal, signer2: principal, required-sigs: uint
depositpublicvault-id: uint, amount: uint
propose-withdrawalpublicvault-id: uint, recipient: principal, amount: uint
sign-proposalpublicproposal-id: uint
execute-withdrawalprivateproposal-id: uint