Source Code

;; Title: MultiSafe
;; Author: Talha Bugra Bulut & Trust Machines
;;
;; Synopsis:
;; A multi-owner contract to manage Stacks Blockchain resources that requires n number of confirmations.
;; Owners submit new transactions specifying a target executor function of a smart contract that implements
;; executor-trait interface. The executor function gets triggered along with two parameters (param-p a principal 
;; parameter and param-u an uint parameter) when the transaction receive sufficient number of confirmations from 
;; owners. The target executor function can execute any kind of code with authority of the safe contract instance
;; such as STX transfer, sip-009-nft transfer, sip-010-trait-ft transfer and much more. Owners list limited to 20 
;; members at maximum considering a realistic use case for this kind of multi-owner safe contract.

(use-trait executor-trait 'SP282BC63F7JNK71YCF7HZHZZ2T9S9P3BN5ZAS3B6.multisafe-traits.executor-trait) 
(use-trait safe-trait 'SP282BC63F7JNK71YCF7HZHZZ2T9S9P3BN5ZAS3B6.multisafe-traits.safe-trait)
(use-trait nft-trait 'SP282BC63F7JNK71YCF7HZHZZ2T9S9P3BN5ZAS3B6.multisafe-traits.sip-009-trait)
(use-trait ft-trait 'SP282BC63F7JNK71YCF7HZHZZ2T9S9P3BN5ZAS3B6.multisafe-traits.sip-010-trait)
(use-trait magic-bridge-trait 'SP282BC63F7JNK71YCF7HZHZZ2T9S9P3BN5ZAS3B6.multisafe-traits.magic-bridge-trait)

(impl-trait 'SP282BC63F7JNK71YCF7HZHZZ2T9S9P3BN5ZAS3B6.multisafe-traits.safe-trait)

;; Errors
(define-constant ERR-CALLER-MUST-BE-SELF (err u100))
(define-constant ERR-OWNER-ALREADY-EXISTS (err u110))
(define-constant ERR-OWNER-NOT-EXISTS (err u120))
(define-constant ERR-UNAUTHORIZED-SENDER (err u130))
(define-constant ERR-ONLY-END-USER (err u135))
(define-constant ERR-TX-NOT-FOUND (err u140))
(define-constant ERR-TX-ALREADY-CONFIRMED-BY-OWNER (err u150))
(define-constant ERR-TX-INVALID-EXECUTOR (err u160))
(define-constant ERR-INVALID-SAFE (err u170))
(define-constant ERR-TX-CONFIRMED (err u180))
(define-constant ERR-TX-NOT-CONFIRMED-BY-SENDER (err u190))
(define-constant ERR-THRESHOLD-CANT-BE-ZERO (err u210))
(define-constant ERR-OWNER-OVERFLOW (err u220))
(define-constant ERR-THRESHOLD-OVERFLOW (err u230))
(define-constant ERR-TX-INVALID-FT (err u240))
(define-constant ERR-TX-INVALID-NFT (err u250))
(define-constant ERR-MB-ADDRESS-NOT-SET (err u260))
(define-constant ERR-INVALID-MB-ADDRESS (err u270))

;; Principal of deployed contract
(define-constant SELF (as-contract tx-sender))

;; --- Version

;; Version string
(define-constant VERSION "0.0.5.beta")

;; Returns version of the safe contract
;; @returns string-ascii
(define-read-only (get-version) 
    VERSION
)

;; --- Owners

;; The owners list
(define-data-var owners (list 20 principal) (list)) 

;; Returns owner list
;; @returns list
(define-read-only (get-owners)
    (var-get owners)
)

;; Private function to push a new member to the owners list
;; @params owner
;; @returns bool
(define-private (add-owner-internal (owner principal))
    (let 
        (
           (new-owners (unwrap! (as-max-len? (append (var-get owners) owner) u20) ERR-OWNER-OVERFLOW))
        )
        (ok (var-set owners new-owners))
    )
)

;; Adds new owner
;; @restricted to SELF
;; @params owner
;; @returns (response bool)
(define-public (add-owner (owner principal))
    (begin
        (asserts! (is-eq tx-sender SELF) ERR-CALLER-MUST-BE-SELF)
        (asserts! (is-none (index-of (var-get owners) owner)) ERR-OWNER-ALREADY-EXISTS)
        (add-owner-internal owner)
    )
)

;; A helper variable to filter owners while removing one
(define-data-var rem-owner principal tx-sender)

;; Returns a new owner list removing the given as parameter
;; @param owner
;; @returns list
(define-private (remove-owner-filter (owner principal)) (not (is-eq owner (var-get rem-owner))))

;; Removes an owner
;; @restricted to SELF
;; @params owner
;; @returns (response bool)
(define-public (remove-owner (owner principal))
    (let
        (
            (owners-list (var-get owners))
        )
        (asserts! (is-eq tx-sender SELF) ERR-CALLER-MUST-BE-SELF)
        (asserts! (is-some (index-of owners-list owner)) ERR-OWNER-NOT-EXISTS)
        (asserts! (>= (- (len owners-list) u1) (var-get threshold)) ERR-THRESHOLD-OVERFLOW)
        (var-set rem-owner owner)
        (ok (var-set owners (unwrap-panic (as-max-len? (filter remove-owner-filter owners-list) u20))))
    )
)


;; --- Minimum confirmation threshold 

(define-data-var threshold uint u1)

;; Returns confirmation threshold
;; @returns uint 
(define-read-only (get-threshold)
    (var-get threshold)
)

;; Private function to set confirmation threshold
;; @params value
;; return bool
(define-private (set-threshold-internal (value uint))
    (var-set threshold value)
)

;; Updates minimum confirmation threshold
;; @restricted to SELF
;; @params value
;; @returns (response bool)
(define-public (set-threshold (value uint))
    (begin
        (asserts! (is-eq tx-sender SELF) ERR-CALLER-MUST-BE-SELF)
        (asserts! (> value u0) ERR-THRESHOLD-CANT-BE-ZERO)
        (asserts! (<= value (len (var-get owners))) ERR-THRESHOLD-OVERFLOW)
        (ok (set-threshold-internal value))
    )
)

;; --- Nonce

;; Incrementing number to use as id for new transactions
(define-data-var nonce uint u0)

;; Returns nonce 
;; @returns uint
(define-read-only (get-nonce)
 (var-get nonce)
)

;; Increases nonce
;; @returns bool
(define-private (increase-nonce)
    (var-set nonce (+ (var-get nonce) u1))
)


;; --- Access control

;; A map to store allowed contract addresses
(define-map allowed-callers principal bool)

;; Adds an address to allowed-callers map
;; @restricted to SELF
;; @params principal
;; @returns (response bool)
(define-public (allow-caller (caller principal))
  (begin
    (asserts! (is-eq tx-sender SELF) ERR-CALLER-MUST-BE-SELF)
    (ok (map-set allowed-callers caller true))
  )
)

;; Removes an address from allowed-callers map
;; @restricted to SELF
;; @params principal
;; @returns (response bool)
(define-public (revoke-caller (caller principal))
  (begin
    (asserts! (is-eq tx-sender SELF) ERR-CALLER-MUST-BE-SELF)
    (ok (map-delete allowed-callers caller))
  )
)

;; Returns true if the caller passed in the allowed-callers map 
;; or its equal to current tx-sender
;; @returns bool
(define-read-only (is-allowed-caller (caller principal))
  (or
    (match (map-get? allowed-callers caller)
      value true
      false
    )
    (is-eq tx-sender caller)
  )
)

;; --- Read all basic safe information at once

(define-read-only (get-info)
    (ok {
        version: (get-version),
        owners: (get-owners),
        threshold: (get-threshold),
        nonce: (get-nonce),
        mb-address: (get-mb-address)
    })
)


;; --- Transactions

;; SOME NOTES ON DESIGN
;; It's not possible to get principal of an optional trait parameter using `contract-of` function.
;; Also trait references cannot be stored on clarity contracts either.
;; That's why we can't have optional `param-ft` and `param-nft` while having optional directives for `param-p`, `param-u` and `param-b`.

(define-map transactions 
    uint 
    {
        executor: principal,
        threshold: uint,
        confirmations: (list 20 principal),
        confirmed: bool,
        param-ft: principal,
        param-nft: principal,
        param-p: (optional principal),
        param-u: (optional uint),
        param-b: (optional (buff 20))
    }
)

;; Private function to insert a new transaction into transactions map
;; @params executor ; contract address to be executed
;; @params param-ft ; fungible token reference for token transfers
;; @params param-nft ; non-Fungible token reference for token transfers
;; @params param-p ; optional principal parameter to be passed to the executor function
;; @params param-u ; optional uint parameter to be passed to the executor function
;; @params param-b ; optional buffer parameter to be passed to the executor function
;; @returns uint
(define-private (add (executor <executor-trait>) (param-ft <ft-trait>) (param-nft <nft-trait>) (param-p (optional principal)) (param-u (optional uint)) (param-b (optional (buff 20))))
    (let 
        (
            (tx-id (get-nonce))
        ) 
        (map-insert transactions tx-id {
            executor: (contract-of executor),
            threshold: (var-get threshold), 
            confirmations: (list), 
            confirmed: false,
            param-ft: (contract-of param-ft),
            param-nft: (contract-of param-nft),
            param-p: param-p,
            param-u: param-u,
            param-b: param-b
        })
        (increase-nonce)
        tx-id
    )
)

;; Returns a transaction by id
;; @params tx-id ; transaction id
;; @returns tuple
(define-read-only (get-transaction (tx-id uint))
    (merge {id: tx-id} (unwrap-panic (map-get? transactions tx-id)))
)

;; Returns transactions by ids
;; @params tx-ids ; transaction id list
;; @returns list
(define-read-only (get-transactions (tx-ids (list 20 uint)))
    (map get-transaction tx-ids)
)

;; A helper variable to filter confirmations while removing one
(define-data-var rem-confirmation principal tx-sender)

;; Returns a new confirmations list removing the given as parameter
;; @param owner
;; @returns list
(define-private (remove-confirmation-filter (owner principal)) (not (is-eq owner (var-get rem-confirmation))))


;; Allows an owner to remove their confirmation on the transaction
;; @restricted to owner who confirmed the transaction before
;; @params tx-id ; transaction id
;; @returns (response bool)
(define-public (revoke (tx-id uint))
    (let 
        (
            (tx (unwrap! (map-get? transactions tx-id) ERR-TX-NOT-FOUND))
            (confirmations (get confirmations tx))
        )
        (asserts! (is-allowed-caller contract-caller) ERR-ONLY-END-USER)
        (asserts! (is-eq (get confirmed tx) false) ERR-TX-CONFIRMED)
        (asserts! (is-some (index-of confirmations tx-sender)) ERR-TX-NOT-CONFIRMED-BY-SENDER)
        (var-set rem-confirmation tx-sender)
        (let 
            (
                (new-confirmations  (unwrap-panic (as-max-len? (filter remove-confirmation-filter confirmations) u20)))
                (new-tx (merge tx {confirmations: new-confirmations}))
            )
            (map-set transactions tx-id new-tx)
            (print {action: "multisafe-revoke", sender: tx-sender, tx-id: tx-id})
            (ok true)
        )
    )
)


;; Allows an owner to confirm a tranaction. If the transaction reaches sufficient confirmation number 
;; then the executor specified on the transaction gets triggered.
;; @restricted to owners who hasn't confirmed the transaction yet
;; @params executor ; contract address to be executed
;; @params safe ; address of safe instance / SELF
;; @params param-ft ; fungible token reference for token transfers
;; @params param-nft ; non-fungible token reference for token transfers
;; @returns (response bool)
(define-public (confirm (tx-id uint) (executor <executor-trait>) (safe <safe-trait>) (param-ft <ft-trait>) (param-nft <nft-trait>))
    (begin
        (asserts! (is-allowed-caller contract-caller) ERR-ONLY-END-USER)
        (asserts! (is-some (index-of (var-get owners) tx-sender)) ERR-UNAUTHORIZED-SENDER)
        (asserts! (is-eq (contract-of safe) SELF) ERR-INVALID-SAFE) 
        (let
            (
                (tx (unwrap! (map-get? transactions tx-id) ERR-TX-NOT-FOUND))
                (confirmations (get confirmations tx))
            )

            (asserts! (is-eq (get confirmed tx) false) ERR-TX-CONFIRMED)
            (asserts! (is-none (index-of confirmations tx-sender)) ERR-TX-ALREADY-CONFIRMED-BY-OWNER)
            (asserts! (is-eq (get executor tx) (contract-of executor)) ERR-TX-INVALID-EXECUTOR)
            (asserts! (is-eq (get param-ft tx) (contract-of param-ft)) ERR-TX-INVALID-FT)
            (asserts! (is-eq (get param-nft tx) (contract-of param-nft)) ERR-TX-INVALID-NFT)
            
            (let 
                (
                    (new-confirmations (unwrap-panic (as-max-len? (append confirmations tx-sender) u20)))
                    (confirmed (>= (len new-confirmations) (get threshold tx)))
                    (new-tx (merge tx {confirmations: new-confirmations, confirmed: confirmed}))
                )
                (map-set transactions tx-id new-tx)
                (and confirmed (try! (as-contract (contract-call? executor execute safe param-ft param-nft (get param-p tx) (get param-u tx) (get param-b tx)))))
                (print {action: "multisafe-confirmation", sender: tx-sender, tx-id: tx-id, confirmed: confirmed})
                (ok confirmed)
            )
        )
    )
)

;; Allows an owner to add a new transaction and confirms it for the owner who submitted it. 
;; So, a newly submitted transaction gets one confirmation automatically. If the safe's minimum
;; required confirmation number is one then the transaction gets executed in this step.
;; @restricted to owners
;; @params executor ; contract address to be executed
;; @params safe ; address of safe instance / SELF
;; @params param-ft ; fungible token reference for token transfers
;; @params param-nft ; non-Fungible token reference for token transfers
;; @params param-p ; optional principal parameter to be passed to the executor function
;; @params param-u ; optional uint parameter to be passed to the executor function
;; @params param-u ; optional buffer parameter to be passed to the executor function
;; @returns (response uint)
(define-public (submit (executor <executor-trait>) (safe <safe-trait>) (param-ft <ft-trait>) (param-nft <nft-trait>) (param-p (optional principal)) (param-u (optional uint)) (param-b (optional (buff 20))))
    (begin
        (asserts! (is-allowed-caller contract-caller) ERR-ONLY-END-USER)
        (asserts! (is-some (index-of (var-get owners) tx-sender)) ERR-UNAUTHORIZED-SENDER)
        (asserts! (is-eq (contract-of safe) SELF) ERR-INVALID-SAFE) 
        (let
            ((tx-id (add executor param-ft param-nft param-p param-u param-b)))
            (print {action: "multisafe-submit", sender: tx-sender, tx-id: tx-id, executor: executor, param-ft: param-ft, param-nft: param-nft, param-p: param-p, param-u: param-u, param-b: param-b})
            (unwrap-panic (confirm tx-id executor safe param-ft param-nft))
            (ok tx-id)
        )
    )
)

;; --- Magic Bridge integration

;; Magic Bridge contract address. 
;; By default address of deployed safe to be able to use none-optional principal.
(define-data-var mb-address principal SELF)

;; Updates magic bridge contract address
;; @restricted to SELF
;; @params address
;; @returns (response bool)
(define-public (set-mb-address (address principal))
    (begin
        (asserts! (is-eq tx-sender SELF) ERR-CALLER-MUST-BE-SELF)
        (ok (var-set mb-address address))
    )
)

;; Returns magic bridge contract address 
;; @returns principal
(define-read-only (get-mb-address)
 (var-get mb-address)
)

;; Registers the safe as a swapper to Magic Bridge.
;; @restricted to owners
;; @params bridge ; contract address of Magic Bridge
;; @returns (response bool)
(define-public (mb-initialize-swapper (bridge <magic-bridge-trait>))
    (begin
        (asserts! (not (is-eq (var-get mb-address) SELF)) ERR-MB-ADDRESS-NOT-SET)
        (asserts! (is-eq (contract-of bridge) (var-get mb-address)) ERR-INVALID-MB-ADDRESS)
        (asserts! (is-some (index-of (var-get owners) tx-sender)) ERR-UNAUTHORIZED-SENDER)
        (try! (as-contract (contract-call? bridge initialize-swapper)))
        (ok true)
    )
)

;; Escrow funds for a supplier after sending BTC during an inbound swap.
;; @params bridge ; contract address of Magic Bridge
;; @param block; a tuple containing `header` (the Bitcoin block header) and the `height` (Stacks height)
;; where the BTC tx was confirmed.
;; @param prev-blocks; because Clarity contracts can't get Bitcoin headers when there is no Stacks block,
;; this param allows users to specify the chain of block headers going back to the block where the
;; BTC tx was confirmed.
;; @param tx; the hex data of the BTC tx
;; @param proof; a merkle proof to validate inclusion of this tx in the BTC block
;; @param output-index; the index of the HTLC output in the BTC tx
;; @param sender; The swapper's public key used in the HTLC
;; @param recipient; The supplier's public key used in the HTLC
;; @param expiration-buff; A 4-byte integer the indicated the expiration of the HTLC
;; @param hash; a hash of the `preimage` used in this swap
;; @param swapper-buff; a 4-byte integer that indicates the `swapper-id`
;; @param supplier-id; the supplier used in this swap
;; @param min-to-receive; minimum receivable calculated off-chain to avoid the supplier front-run the swap by adjusting fees
;; @returns (response bool)
(define-public (mb-escrow-swap 
    (bridge <magic-bridge-trait>)
    (block { header: (buff 80), height: uint })
    (prev-blocks (list 10 (buff 80)))
    (tx (buff 1024))
    (proof { tx-index: uint, hashes: (list 12 (buff 32)), tree-depth: uint })
    (output-index uint)
    (sender (buff 33))
    (recipient (buff 33))
    (expiration-buff (buff 4))
    (hash (buff 32))
    (swapper-buff (buff 4))
    (supplier-id uint)
    (min-to-receive uint)
  )
    (begin
        (asserts! (not (is-eq (var-get mb-address) SELF)) ERR-MB-ADDRESS-NOT-SET)
        (asserts! (is-eq (contract-of bridge) (var-get mb-address)) ERR-INVALID-MB-ADDRESS)
        (asserts! (is-some (index-of (var-get owners) tx-sender)) ERR-UNAUTHORIZED-SENDER)
        (try! (as-contract (contract-call? bridge escrow-swap block prev-blocks tx proof output-index sender recipient expiration-buff hash swapper-buff supplier-id min-to-receive)))
        (ok true)
    )
)

;; Safe initializer
;; @params o ; owners list
;; @params m ; minimum required confirmation number
(define-private (init (o (list 20 principal)) (m uint))
    (begin
        (map add-owner-internal o)
        (set-threshold-internal m)
        (print {action: "multisafe-init"})
    )
)

(init (list
 'SP2C20XGZBAYFZ1NYNHT1J6MGMM0EW9X7PFBWK7QG
 'SP27ENRYMEGM9K6TVR7A8JEDTFXN5EA22KPJHVPE1 
) u1)  

Functions (26)

FunctionAccessArgs
get-versionread-only
get-ownersread-only
add-owner-internalprivateowner: principal
add-ownerpublicowner: principal
remove-owner-filterprivateowner: principal
remove-ownerpublicowner: principal
get-thresholdread-only
set-threshold-internalprivatevalue: uint
set-thresholdpublicvalue: uint
get-nonceread-only
increase-nonceprivate
allow-callerpubliccaller: principal
revoke-callerpubliccaller: principal
is-allowed-callerread-onlycaller: principal
get-inforead-only
addprivateexecutor: <executor-trait>, param-ft: <ft-trait>, param-nft: <nft-trait>, param-p: (optional principal
get-transactionread-onlytx-id: uint
get-transactionsread-onlytx-ids: (list 20 uint
remove-confirmation-filterprivateowner: principal
revokepublictx-id: uint
confirmpublictx-id: uint, executor: <executor-trait>, safe: <safe-trait>, param-ft: <ft-trait>, param-nft: <nft-trait>
submitpublicexecutor: <executor-trait>, safe: <safe-trait>, param-ft: <ft-trait>, param-nft: <nft-trait>, param-p: (optional principal
set-mb-addresspublicaddress: principal
get-mb-addressread-only
mb-initialize-swapperpublicbridge: <magic-bridge-trait>
initprivateo: (list 20 principal