Source Code

;; This is a vault contract. It is the only contract needed per client per chain & is
;; therefore used as a universal address. It functions by allow a batch of verified
;; signatures to execute a transaction or transfer by checking against a policy.
;; A transaction is a contract call meant for protocol interactions.
;; A transfer is strictly a sip10 token transfer from the vault to a recipient.

;; cons
(define-constant SIP018_MSG_PREFIX 0x534950303138)
(define-constant CLIENT 0x16cbd0716887fd9259f39d403e19eb3436e3bdf3c17a37035cf0f8f0d7851e0b)
(use-trait wrapper-trait 'SPM5G9CE1RAEEXPXQSB8QPQP6Y9F67MP61146FBA.cf-traits-v0.wrapper-trait)
(use-trait token-trait 'SPM5G9CE1RAEEXPXQSB8QPQP6Y9F67MP61146FBA.sip-010-trait-ft-standard.sip-010-trait)

;; errs
(define-constant ERR_UNAUTHORIZED_USER (err u100))
(define-constant ERR_INACTIVE_POLICY (err u101))
(define-constant ERR_INVALID_HELPER (err u102))
(define-constant ERR_THRESHOLD_NOT_MET (err u103))
(define-constant ERR_INVALID_TYPE (err u104))
(define-constant ERR_AMOUNT_EXCEEDS_POLICY (err u105))
(define-constant ERR_INVALID_USER (err u106))
(define-constant ERR_INACTIVE_USER (err u107))
(define-constant ERR_INVALID_POLICY (err u108))
(define-constant ERR_INVALID_SIGNATURE (err u109))
(define-constant ERR_INVALID_SIGNER (err u110))
(define-constant ERR_AUTH_ID_REPLAY (err u111))

;; execute-transaction
;; This function executes a transfer out of this contract based on an enforced policy & a list of signatures.
;; If the transaction (not transfer), the serialized tuple/params buff should appended to the message.
(define-public (execute-transaction (user-id (string-ascii 64))
                                    (contract <wrapper-trait>)
                                    (name (string-ascii 32))
                                    (instructions (buff 4096))
                                    (policy-id (string-ascii 64))
                                    (auth-id (string-ascii 64))
                                    (type (string-ascii 32))
                                    (signed-data (list 35 {signer: (buff 33), signature: (buff 65)})))
    (let
        (
            ;; Fetch & check for user
            (user (unwrap! (contract-call? 'SPM5G9CE1RAEEXPXQSB8QPQP6Y9F67MP61146FBA.cf-helpers-state-v0 get-user CLIENT user-id) ERR_INVALID_USER))
            ;; Fetch & check for valid policy
            (policy (unwrap! (contract-call? 'SPM5G9CE1RAEEXPXQSB8QPQP6Y9F67MP61146FBA.cf-helpers-state-v0 get-policy CLIENT policy-id) ERR_INVALID_POLICY))
            ;; Unwrap transaction properties
            (transaction-policy (unwrap! (get transaction policy) ERR_INVALID_TYPE))
        )
        ;; Check that user is active
        (asserts! (get active user) ERR_INACTIVE_USER)
        ;; Check that policy is active
        (asserts! (get active policy) ERR_INACTIVE_POLICY)
        ;; Check that all signatures are valid & all signers are in the policy
        (try! (verify-transaction-signature policy-id (get signers policy) type (contract-of contract) name auth-id signed-data))
        ;; Check that signing threshold is met
        (asserts! (>= (len signed-data) (get threshold policy)) ERR_THRESHOLD_NOT_MET)
        ;; Cofund wrapper generic contract call
        (try! (contract-call? contract router-wrapper name instructions))
        (print {
            topic: "Transaction Executed",
            policy-id: policy-id,
            type: type,
            function: name,
            wrapper: (contract-of contract),
            auth-id: auth-id
        })
        (ok true)
    )
)

;; execute-transaction
;; This function executes a transfer out of this contract based on an enforced policy & a list of signatures.
(define-public (execute-transfer (user-id (string-ascii 64))
                                    (policy-id (string-ascii 64))
                                    (type (string-ascii 32))
                                    (amount uint)
                                    (token <token-trait>)
                                    (recipient principal)
                                    (auth-id (string-ascii 64))
                                    (signed-data (list 35 {signer: (buff 33), signature: (buff 65)})))
    (let
        (
            ;; Fetch & check for user
            (user (unwrap! (contract-call? 'SPM5G9CE1RAEEXPXQSB8QPQP6Y9F67MP61146FBA.cf-helpers-state-v0 get-user CLIENT user-id) ERR_INVALID_USER))
            ;; Fetch & check for valid policy
            (policy (unwrap! (contract-call? 'SPM5G9CE1RAEEXPXQSB8QPQP6Y9F67MP61146FBA.cf-helpers-state-v0 get-policy CLIENT policy-id) ERR_INVALID_POLICY))
            ;; Unwrap transfer properties
            (transfer-policy (unwrap! (get transfer policy) ERR_INVALID_TYPE))
        )
        ;; Check that user is active
        (asserts! (get active user) ERR_INACTIVE_USER)
        ;; Check that policy is active
        (asserts! (get active policy) ERR_INACTIVE_POLICY)
        ;; Check that all signatures are valid & all signers are in the policy
        (try! (verify-transfer-signature policy-id (get signers policy) type amount (contract-of token) recipient auth-id signed-data))
        ;; Check that signing threshold is met
        (asserts! (>= (len signed-data) (get threshold policy)) ERR_THRESHOLD_NOT_MET)
        ;; Check that amount is less than max-amount
        (asserts! (<= amount (get max-amount transfer-policy)) ERR_AMOUNT_EXCEEDS_POLICY)
        ;; Transfer tokens from vault to recipient
        (try! (as-contract (contract-call? token transfer amount tx-sender recipient none)))
        (print {
            topic: "Transfer Executed",
            policy-id: policy-id,
            type: type,
            amount: amount,
            token: token,
            recipient: recipient,
            auth-id: auth-id
        })
        (ok true)
    )
)

;; TODO: Add 'instructions' to the transaction message/signature
;; verify-transaction
;; The following functions verify batched transaction signatures
;; Meant for interacting with on-chain contracts
;; verify-transaction-signature
(define-private (verify-transaction-signature (policy-id (string-ascii 64))
                                    (policy-signers (list 35 (buff 33)))
                                    (type (string-ascii 32))
                                    (wrapper principal)
                                    (function (string-ascii 32))
                                    (auth-id (string-ascii 64))
                                    (signed-data (list 35 {signer: (buff 33), signature: (buff 65)})))
    (begin
        ;; verify fresh auth-id
        (asserts! (is-none (contract-call? 'SPM5G9CE1RAEEXPXQSB8QPQP6Y9F67MP61146FBA.cf-helpers-state-v0 get-used-auth-ids CLIENT auth-id)) ERR_AUTH_ID_REPLAY)
        ;; Check that all signatures are valid & all signers are in the policy
        (try! (fold verify-transaction-signature-helper signed-data (ok {type: type, wrapper: wrapper, function: function, auth-id: auth-id, policy-id: policy-id, policy-signers: policy-signers})))
        ;; update auth-id
        (unwrap! (contract-call? 'SPM5G9CE1RAEEXPXQSB8QPQP6Y9F67MP61146FBA.cf-helpers-state-v0 set-auth-id CLIENT "users" auth-id) ERR_AUTH_ID_REPLAY)
        (ok true)
    )
)

;; verify-transaction-signature-helper
;; This function verifies signature & checks that the signer is in the policy signer set.
(define-private (verify-transaction-signature-helper (signed-data {signer: (buff 33), signature: (buff 65)})
                                  (signed-response (response {type: (string-ascii 32), wrapper: principal, function: (string-ascii 32), auth-id: (string-ascii 64), policy-id: (string-ascii 64), policy-signers: (list 35 (buff 33))} uint)))
    (match signed-response
        ok-response
        (begin
            ;; verify signature
            (asserts! (read-valid-transaction-signature (get policy-id ok-response) (get type ok-response) (get wrapper ok-response) (get function ok-response) (get auth-id ok-response) (get signature signed-data) (get signer signed-data)) ERR_INVALID_SIGNATURE)
            ;; verify signer in policy signer set
            (unwrap! (index-of? (get policy-signers ok-response) (get signer signed-data)) ERR_INVALID_SIGNER)
            (ok ok-response)
        )
        err-response
        (err err-response)
    )
)

;; read-valid-transaction-signature
;; Verify one signature from the list of signatures for an attempted transaction.
;; See `get-signer-key-message-hash` for details on the message hash.
(define-read-only (read-valid-transaction-signature (policy-id (string-ascii 64))
                                        (type (string-ascii 32))
                                        (wrapper principal)
                                        (function (string-ascii 32))
                                        (auth-id (string-ascii 64))
                                        (signature (buff 65))
                                        (signer-key (buff 33)))
    (secp256k1-verify (get-transaction-signature-message-hash policy-id type wrapper function auth-id) signature signer-key)
)

;; get-transaction-signature-message-hash
;; Generate a transaction message hash following SIP018 for signing structured data.
;; The domain is `{ name: "cofund-signer", version: "1.0.0", chain-id: chain-id }`.
(define-read-only (get-transaction-signature-message-hash (policy-id (string-ascii 64))
                                               (type (string-ascii 32))
                                               (wrapper principal)
                                               (function (string-ascii 32))
                                               (auth-id (string-ascii 64)))
  (sha256 (concat
    SIP018_MSG_PREFIX
    (concat
      (sha256 (unwrap-panic (to-consensus-buff? { name: "cofund-signer", version: "1.0.0", chain-id: u1 })))
      (sha256 (unwrap-panic
        (to-consensus-buff? {
          auth-id: auth-id,
          function: function,
          policy-id: policy-id,
          type: type,
          wrapper: wrapper,
        })))
    ))
  ))

;; verify-transfer
;; the following functions verify batched transfer signatures
;; verify-transfer-signature
(define-private (verify-transfer-signature (policy-id (string-ascii 64))
                                    (policy-signers (list 35 (buff 33)))
                                    (type (string-ascii 32))
                                    (amount uint)
                                    (token principal)
                                    (recipient principal)
                                    (auth-id (string-ascii 64))
                                    (signed-data (list 35 {signer: (buff 33), signature: (buff 65)})))
    (begin
        ;; verify fresh auth-id
        (asserts! (is-none (contract-call? 'SPM5G9CE1RAEEXPXQSB8QPQP6Y9F67MP61146FBA.cf-helpers-state-v0 get-used-auth-ids CLIENT auth-id)) ERR_AUTH_ID_REPLAY)
        ;; Check that all signatures are valid & all signers are in the policy
        (try! (fold verify-transfer-signature-helper signed-data (ok {type: type, amount: amount, token: token, recipient: recipient, auth-id: auth-id, policy-id: policy-id, policy-signers: policy-signers})))
        ;; update auth-id
        (unwrap! (contract-call? 'SPM5G9CE1RAEEXPXQSB8QPQP6Y9F67MP61146FBA.cf-helpers-state-v0 set-auth-id CLIENT "users" auth-id) ERR_AUTH_ID_REPLAY)
        (ok true)
    )
)
;; verify-transfer-signature
;; This function verifies signature & checks that the signer is in the policy signer set.
(define-private (verify-transfer-signature-helper (signed-data {signer: (buff 33), signature: (buff 65)})
                                  (signed-response (response {type: (string-ascii 32), amount: uint, token: principal, recipient: principal, auth-id: (string-ascii 64), policy-id: (string-ascii 64), policy-signers: (list 35 (buff 33))} uint)))
    (match signed-response
        ok-response
        (begin
            ;; TODO: Verify that the signer is an admin or employee
            ;; verify signature
            (asserts! (read-valid-transfer-signature (get policy-id ok-response) (get type ok-response) (get amount ok-response) (get token ok-response) (get recipient ok-response) (get auth-id ok-response) (get signature signed-data) (get signer signed-data)) ERR_INVALID_SIGNATURE)
            ;; verify signer in policy signer set
            (unwrap! (index-of? (get policy-signers ok-response) (get signer signed-data)) ERR_INVALID_SIGNER)
            (ok ok-response)
        )
        err-response
        (err err-response)
    )
)
;; read-valid-transfer-signature
;; Verify one signature from the list of signatures for an attempted transaction.
;; See `get-signer-key-message-hash` for details on the message hash.
(define-read-only (read-valid-transfer-signature (policy-id (string-ascii 64))
                                        (type (string-ascii 32))
                                        (amount uint)
                                        (token principal)
                                        (recipient principal)
                                        (auth-id (string-ascii 64))
                                        (signature (buff 65))
                                        (signer-key (buff 33)))

        (secp256k1-verify (get-transfer-signature-message-hash policy-id type amount token recipient auth-id) signature signer-key)
)
;; get-transfer-signature-message-hash
;; Generate a transfer message hash following SIP018 for signing structured data.
;; The domain is `{ name: "cofund-signer", version: "1.0.0", chain-id: chain-id }`.
(define-read-only (get-transfer-signature-message-hash (policy-id (string-ascii 64))
                                               (type (string-ascii 32))
                                               (amount uint)
                                               (token principal)
                                               (recipient principal)
                                               (auth-id (string-ascii 64)))
  (sha256 (concat
    SIP018_MSG_PREFIX
    (concat
      (sha256 (unwrap-panic (to-consensus-buff? { name: "cofund-signer", version: "1.0.0", chain-id: u1 })))
      (sha256 (unwrap-panic
        (to-consensus-buff? {
          amount: amount,
          auth-id: auth-id,
          policy-id: policy-id,
          recipient: recipient,
          token: token,
          type: type,
        })))
    ))
  )
)

(contract-call? 'SPM5G9CE1RAEEXPXQSB8QPQP6Y9F67MP61146FBA.cf-helpers-state-v0 new-client CLIENT 0x5880d7bf9d9e78d2a8bffe1b246ec8c2750eeb685eb47f0f2e86460ed1d21b4a "4103b8a2-8403-4ef0-af6a-09ae9b7ee684")

Functions (8)

FunctionAccessArgs
execute-transactionpublicuser-id: (string-ascii 64
execute-transferpublicuser-id: (string-ascii 64
verify-transaction-signatureprivatepolicy-id: (string-ascii 64
read-valid-transaction-signatureread-onlypolicy-id: (string-ascii 64
get-transaction-signature-message-hashread-onlypolicy-id: (string-ascii 64
verify-transfer-signatureprivatepolicy-id: (string-ascii 64
read-valid-transfer-signatureread-onlypolicy-id: (string-ascii 64
get-transfer-signature-message-hashread-onlypolicy-id: (string-ascii 64