Source Code

;; Title: Multi-Safe
;; 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 specifiying 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 
;; paramter and param-u an uint paramter) 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 tranfer, 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 'SP3XD84X3PE79SHJAZCDW1V5E9EA8JSKRBPEKAEK7.multisafe-traits.executor-trait) 
(use-trait safe-trait 'SP3XD84X3PE79SHJAZCDW1V5E9EA8JSKRBPEKAEK7.multisafe-traits.safe-trait)

(impl-trait 'SP3XD84X3PE79SHJAZCDW1V5E9EA8JSKRBPEKAEK7.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-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-AT-LEAST-ONE-OWNER-REQUIRED (err u200))
(define-constant ERR-MIN-CONFIRMATION-CANT-BE-ZERO (err u210))


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

;; --- Version

;; Version string
(define-constant VERSION "0.0.1.alpha")

;; 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))
    (var-set owners (unwrap-panic (as-max-len? (append (var-get owners) owner) u20)))
)

;; 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)
        (ok (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) ERR-AT-LEAST-ONE-OWNER-REQUIRED)
        (var-set rem-owner owner)
        (ok (var-set owners (unwrap-panic (as-max-len? (filter remove-owner-filter owners-list) u20))))
    )
)


;; --- Minimum confirmation requirement 

(define-data-var min-confirmation uint u0)

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

;; Private function to set minimum required confirmation number
;; @params value
;; return bool
(define-private (set-min-confirmation-internal (value uint))
    (var-set min-confirmation value)
)

;; Updates minimum required confirmation number
;; @restricted to SELF
;; @params value
;; @returns (response bool)
(define-public (set-min-confirmation (value uint))
    (begin
        (asserts! (is-eq tx-sender SELF) ERR-CALLER-MUST-BE-SELF)
        (asserts! (> value u0) ERR-MIN-CONFIRMATION-CANT-BE-ZERO)
        (ok (set-min-confirmation-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))
)

;; --- Transactions

(define-map transactions 
    uint 
    {
        executor: principal,
        confirmations: (list 20 principal),
        confirmed: bool,
        param-p: principal,
        param-u: uint
    }
)

;; Private function to insert a new transaction into transactions map
;; @params executor ; contract address to be executed
;; @params param-p ; principal parameter to be passed to the executor function
;; @params param-u ; uint argument to be passed to the executor function
;; @returns uint
(define-private (add (executor <executor-trait>) (param-p principal) (param-u uint))
    (let 
        (
            (tx-id (get-nonce))
        ) 
        (map-insert transactions tx-id {executor: (contract-of executor), confirmations: (list), confirmed: false, param-p: param-p, param-u: param-u})
        (increase-nonce)
        tx-id
    )
)

;; Returns a transaction by id
;; @params tx-id ; transaction id
;; @returns tuple
(define-read-only (get-transaction (tx-id uint))
    (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-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
;; @returns (response bool)
(define-public (confirm (tx-id uint) (executor <executor-trait>) (safe <safe-trait>))
    (begin
        (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-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)
            
            (let 
                (
                    (new-confirmations (unwrap-panic (as-max-len? (append confirmations tx-sender) u20)))
                    (confirmed (>= (len new-confirmations) (var-get min-confirmation)))
                    (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 (get param-p tx) (get param-u 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-p ; principal parameter to be passed to the executor function
;; @params param-u ; uint argument to be passed to the executor function
;; @returns (response uint)
(define-public (submit (executor <executor-trait>) (safe <safe-trait>) (param-p principal) (param-u uint))
    (begin
        (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-p param-u)))
            (print {action: "multisafe-submit", sender: tx-sender, tx-id: tx-id, executor: executor, param-p: param-p, param-u: param-u})
            (unwrap-panic (confirm tx-id executor safe))
            (ok tx-id)
        )
    )
)


;; 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-min-confirmation-internal m)
        (print {action: "multisafe-init"})
    )
)

(init (list
 'SP5C5J1AVSD63C0PEH965TGFDT1CQWFJY37DTZW5
 'SP160Q17JC5FYE97CC6DTX0NYQN0WKBMCREHEQ9P6
 'SP2EZES4ANJ6JVM0JRQHZ4CYQD2EP1HKVAR340208 
) u2)  

Functions (19)

FunctionAccessArgs
get-versionread-only
get-ownersread-only
add-owner-internalprivateowner: principal
add-ownerpublicowner: principal
remove-owner-filterprivateowner: principal
remove-ownerpublicowner: principal
get-min-confirmationread-only
set-min-confirmation-internalprivatevalue: uint
set-min-confirmationpublicvalue: uint
get-nonceread-only
increase-nonceprivate
addprivateexecutor: <executor-trait>, param-p: principal, param-u: uint
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>
submitpublicexecutor: <executor-trait>, safe: <safe-trait>, param-p: principal, param-u: uint
initprivateo: (list 20 principal