Source Code


;; title: vault
;; version: 0.0.1
;; summary: Vault for storing collateral and constructing bond(s) to lend to protocol(s)
;; description: Vault for storing collateral and constructing bond(s) to lend to protocol(s)

;; traits
;; mainnet - SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard
;; testnet - ST2XX28V6YR45HZJ0D5990MRCBHMGC843GQQ12N1Q
;; devnet - ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM
(use-trait ft-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait)
;;

;; constants
(define-constant contract-owner tx-sender)
;; mainnet - SP1X5C20QNS2RDPRWMXMHJVTGEH1KKSREH2Q6RBWN.sbtc
;; testnet - ST2XX28V6YR45HZJ0D5990MRCBHMGC843GQQ12N1Q.sbtc-2
;; devnet - ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.sbtc
(define-constant collateral-asset-contract 'SP1X5C20QNS2RDPRWMXMHJVTGEH1KKSREH2Q6RBWN.sbtc)
;; mainnet - SP1X5C20QNS2RDPRWMXMHJVTGEH1KKSREH2Q6RBWN
;; testnet - ST2XX28V6YR45HZJ0D5990MRCBHMGC843GQQ12N1Q
;; devnet - ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC
(define-constant tetris-principal 'SP1X5C20QNS2RDPRWMXMHJVTGEH1KKSREH2Q6RBWN)
(define-constant dummy-principal 'ST1HTBVD3JG9C05J7HBJTHGR0GGW7KXW28M5JS8QE) ;;todo - how to have a zero address or something
(define-constant dummy-position {
	id: u0,
	borrower: dummy-principal,
	collateralBalance: u0,
	state: u0,
	lender: dummy-principal,
	liquidator: dummy-principal,
	proofTxId: "",
	tetris: dummy-principal
})
(define-constant dummy-proof-tx-id "1D10T")

;; errors
(define-constant err-owner-only (err u100))
(define-constant err-amount-not-gt-zero (err u102))
(define-constant err-principal-network-mismatch (err u103))
(define-constant err-invalid-position-id (err u104))
(define-constant err-not-a-number (err u105))
(define-constant err-not-enough-collateral (err u106))
(define-constant err-vault-amount-must-gte-than-locked (err u107))
(define-constant err-invalid-vault-state (err u108))
(define-constant err-existing-vault-state (err u109))
(define-constant err-invalid-collateral-state (err u110))
(define-constant err-unauthorized-principal (err u111))
(define-constant err-no-liquidator-defined (err u112))
(define-constant err-no-proof-tx-defined (err u113))


;; max positions for a vault
(define-constant position-ids (list u1 u2 u3 u4 u5 u6 u7 u8 u9 u10))

;; collateral states
(define-constant collateral-state-open u200)
(define-constant collateral-state-locked u201)
(define-constant collateral-state-liquidated u202)
(define-constant collateral-state-archived u203)

;; vault states
(define-constant vault-state-open u300)
(define-constant vault-state-archived u301)

;; data vars
(define-data-var last-position-id uint u0)
(define-data-var current-vault-state uint vault-state-open)
(define-data-var tetris-proof (string-ascii 66) "") ;; todo - this is a replay-proof signature from tetris that the lender needs to validate


;; data maps
(define-map positions
	uint
	{
		id: uint,
		borrower: principal,
        collateralBalance: uint,
        state: uint,
		lender: principal,
		liquidator: principal,
		proofTxId: (string-ascii 66),
		tetris: principal
	}
)
;;

;; public functions
(define-public (deposit (amount uint))
    (let (
		(available-collateral-balance (unwrap! (get-available-collateral-balance) err-not-a-number))
		(event {type: "deposit_to_vault", amount: amount, sender: tx-sender, asset: collateral-asset-contract})
	)

		;; ownership check
        (asserts! (is-eq tx-sender contract-owner) err-owner-only)

		;; vault state
		(asserts! (is-eq (var-get current-vault-state) vault-state-open) err-invalid-vault-state)

		;; context state
        (asserts! (is-eq true (> amount u0)) err-amount-not-gt-zero)
		;; note - we may implement a max and or min deposit amount

        (try! (transfer-ft collateral-asset-contract amount tx-sender (as-contract tx-sender)))
		(print event)
        (ok true)
    )
)

(define-public (withdraw (amount uint))
    (let (
		(available-collateral-balance (unwrap! (get-available-collateral-balance) err-not-a-number))
		(event {type: "withdraw_from_vault", amount: amount, sender: tx-sender, asset: collateral-asset-contract})
	)
	    ;; ownership check
        (asserts! (is-eq tx-sender contract-owner) err-owner-only)

		;; vault check
		;; note - maybe we will allow withdrawal from a closed vault
		(asserts! (is-eq (var-get current-vault-state) vault-state-open) err-invalid-vault-state)

		;; context check
        (asserts! (is-eq true (> amount u0)) err-amount-not-gt-zero)
        (asserts! (is-eq true (>= available-collateral-balance amount)) err-not-enough-collateral)

        (try! (transfer-ft collateral-asset-contract amount (as-contract tx-sender) tx-sender))
		(print event)
        (ok true)
    )
)

(define-public (create-position (collateral-amount uint) (lender principal))
	(let (
		(position-id (+ (var-get last-position-id) u1))
		(available-collateral-balance (unwrap! (get-available-collateral-balance) err-not-a-number))
		(event {type: "create_position", sender: tx-sender, asset: collateral-asset-contract})
	)

		(asserts! (is-standard lender) err-principal-network-mismatch)

	    ;; ownership checks
		(asserts! (is-eq tx-sender contract-owner) err-owner-only)

		;; vault checks
		(asserts! (is-eq (var-get current-vault-state) vault-state-open) err-invalid-vault-state)

		;; context checks
		(asserts! (is-eq true (> collateral-amount u0)) err-amount-not-gt-zero)
		(asserts! (is-eq true (> position-id (var-get last-position-id))) err-invalid-position-id)
		(asserts! (is-eq true (<= position-id (len position-ids))) err-invalid-position-id)
		(asserts! (is-eq true (>= available-collateral-balance collateral-amount)) err-not-enough-collateral)

		(map-set positions position-id {
			id: position-id,
			borrower: tx-sender,
			collateralBalance: collateral-amount,
			state: collateral-state-open,
			lender: lender,
			liquidator: dummy-principal,
			proofTxId: dummy-proof-tx-id,
			tetris: tetris-principal
		})
		(var-set last-position-id position-id)
		(print event)
		(ok true)
	)
)

(define-public (add-to-position (position-id uint) (collateral-amount uint))
	(let (
		(position (unwrap! (map-get? positions position-id) err-invalid-position-id))
		(available-collateral-balance (unwrap! (get-available-collateral-balance) err-not-a-number))
		(event {type: "add_to_position", positionId: position-id, amount: collateral-amount, sender: tx-sender, asset: collateral-asset-contract})
	)
		;; ownership checks
		(asserts! (is-eq tx-sender contract-owner) err-owner-only) ;; only owner can call

		;; vault checks
		(asserts! (is-eq (var-get current-vault-state) vault-state-open) err-invalid-vault-state) ;; vault state must be open

		;; common position checks
		(asserts! (is-eq (get state position) collateral-state-open) err-invalid-collateral-state)
		(asserts! (is-eq tx-sender (get borrower position)) err-owner-only) ;; borrower must match position principal
		(asserts! (is-some (some position)) err-invalid-position-id) ;; position must exist
		(asserts! (is-eq true (<= position-id (var-get last-position-id))) err-invalid-position-id) ;; position must be within valid range

		;; specific context checks
		(asserts! (is-eq true (> collateral-amount u0)) err-amount-not-gt-zero)
		(asserts! (is-eq true (>= available-collateral-balance collateral-amount)) err-not-enough-collateral)

		(map-set positions position-id (merge position {collateralBalance: (+ (get collateralBalance position) collateral-amount)}))
		(print event)
		(ok true)
	)
)

(define-public (remove-from-position (position-id uint) (collateral-amount uint))
	(let (
		(position (unwrap! (map-get? positions position-id) err-invalid-position-id))
		(event {type: "remove_from_position", positionId: position-id, amount: collateral-amount, sender: tx-sender, asset: collateral-asset-contract})
	)

		;; ownership checks
		(asserts! (is-eq tx-sender contract-owner) err-owner-only) ;; only owner can call

		;; vault checks
		(asserts! (is-eq (var-get current-vault-state) vault-state-open) err-invalid-vault-state) ;; vault state must be open

		;; common position checks
		(asserts! (is-eq (get state position) collateral-state-open) err-invalid-collateral-state)
		(asserts! (is-eq tx-sender (get borrower position)) err-owner-only) ;; borrower must match position principal
		(asserts! (is-some (some position)) err-invalid-position-id) ;; position must exist
		(asserts! (is-eq true (<= position-id (var-get last-position-id))) err-invalid-position-id) ;; position must be within valid range

		;; specific context checks
		(asserts! (is-eq true (> collateral-amount u0)) err-amount-not-gt-zero)
		(asserts! (is-eq true (>= (get collateralBalance position) collateral-amount)) err-vault-amount-must-gte-than-locked)
		
		(map-set positions position-id (merge position {collateralBalance: (- (get collateralBalance position) collateral-amount)}))
		(print event)
		(ok true)
	)
)

(define-public (change-vault-state (new-state uint)) 
	(let (
		(event {type: "change_vault_state", sender: tx-sender, oldState: (var-get current-vault-state), newState: new-state })
	) 

		;; ownership check
		(asserts! (is-eq tx-sender contract-owner) err-owner-only)
		
		;; context check
		(asserts! (is-eq true (or (is-eq new-state vault-state-open) (is-eq new-state vault-state-archived))) err-invalid-vault-state)
		(asserts! (is-eq false (is-eq new-state (var-get current-vault-state))) err-existing-vault-state)
		
		(var-set current-vault-state new-state)
		(print event)
		(ok new-state)	
	)
)

(define-public (lock-position (position-id uint))
	(let (
		(position (unwrap! (map-get? positions position-id) err-invalid-position-id))
		(event {type: "lock_position", positionId: position-id, sender: tx-sender})
	)
		;; access checks
		(asserts! (is-eq true (is-eq tx-sender (get lender position))) err-unauthorized-principal)

		;; vault checks
		(asserts! (is-eq (var-get current-vault-state) vault-state-open) err-invalid-vault-state) ;; vault state must be open

		;; common position checks
		(asserts! (is-some (some position)) err-invalid-position-id) ;; position must exist
		(asserts! (is-eq true (<= position-id (var-get last-position-id))) err-invalid-position-id) ;; position must be within valid range

		;; specific context checks
		(asserts! (is-eq (get state position) collateral-state-open) err-invalid-collateral-state) ;; collateral state must be open

		(map-set positions position-id (merge position {state: collateral-state-locked}))
		(print event)
		(ok true)
	)
)

(define-public (liquidate-position (position-id uint) (liquidator principal) (proofTxId (string-ascii 66)))
	(let (
		(position (unwrap! (map-get? positions position-id) err-invalid-position-id))
		(event {type: "liquidate_position", positionId: position-id, sender: tx-sender, liquidator: liquidator, proofTxId: proofTxId})
	)
		;; access checks
		(asserts! (is-eq true (is-eq tx-sender (get lender position))) err-unauthorized-principal)

		;; vault checks
		(asserts! (is-eq (var-get current-vault-state) vault-state-open) err-invalid-vault-state) ;; vault state must be open

		;; common position checks
		(asserts! (is-some (some position)) err-invalid-position-id) ;; position must exist
		(asserts! (is-eq true (<= position-id (var-get last-position-id))) err-invalid-position-id) ;; position must be within valid range

		;; specific context checks
		(asserts! (is-eq (get state position) collateral-state-locked) err-invalid-collateral-state) ;; collateral state must be locked

		(map-set positions position-id (merge position {state: collateral-state-liquidated, liquidator: liquidator, proofTxId: proofTxId}))
		(print event)
		(ok true)
	)
)

(define-public (unlock-position (position-id uint) (proofTxId (string-ascii 66)))
	(let (
		(position (unwrap! (map-get? positions position-id) err-invalid-position-id))
		(event {type: "unlock_position", positionId: position-id, sender: tx-sender, proofTxId: proofTxId})
	)
		;; access checks
		(asserts! (is-eq true (is-eq tx-sender (get lender position))) err-unauthorized-principal)

		;; vault checks
		(asserts! (is-eq (var-get current-vault-state) vault-state-open) err-invalid-vault-state) ;; vault state must be open

		;; common position checks
		(asserts! (is-some (some position)) err-invalid-position-id) ;; position must exist
		(asserts! (is-eq true (<= position-id (var-get last-position-id))) err-invalid-position-id) ;; position must be within valid range

		;; specific context checks
		(asserts! (is-eq (get state position) collateral-state-locked) err-invalid-collateral-state) ;; collateral state must be locked

		(map-set positions position-id (merge position {state: collateral-state-open, proofTxId: proofTxId}))
		(print event)
		(ok true)
	)
)

(define-public (pay-liquidator (position-id uint)) 
	(let (
		(position (unwrap! (map-get? positions position-id) err-invalid-position-id))
		(event {type: "pay_liquidator", positionId: position-id, sender: tx-sender})
	)
		;; access checks
		(asserts! (is-eq true (is-eq tx-sender (get tetris position))) err-unauthorized-principal)
		(asserts! (is-eq true (is-eq tx-sender tetris-principal)) err-unauthorized-principal)

		;; vault checks
		(asserts! (is-eq (var-get current-vault-state) vault-state-open) err-invalid-vault-state)

		;; common position checks
		(asserts! (is-some (some position)) err-invalid-position-id) ;; position must exist
		(asserts! (is-eq true (<= position-id (var-get last-position-id))) err-invalid-position-id) ;; position must be within valid range

		;; specific context checks
		(asserts! (is-eq (get state position) collateral-state-liquidated) err-invalid-collateral-state)
		(asserts! (is-eq false (is-eq (get liquidator position) dummy-principal)) err-no-liquidator-defined)
		(asserts! (is-eq false (is-eq (get proofTxId position) dummy-proof-tx-id)) err-no-proof-tx-defined)

		(try! (transfer-ft collateral-asset-contract (get collateralBalance position) (as-contract tx-sender) (get liquidator position)))
		(map-set positions position-id (merge position {state: collateral-state-archived, collateralBalance: u0}))
		(print event)
		(ok true)
	)
)

(define-public (get-available-collateral-balance)
	(let (
		(vault-balance (unwrap! (balance-ft collateral-asset-contract (as-contract tx-sender)) err-not-a-number)) 
		(locked-balance (unwrap! (get-locked-collateral-balance) err-not-a-number))
	)
    	(ok (if (>= vault-balance locked-balance) (- vault-balance locked-balance) u0))
	)
)

(define-public (get-borrower-position  (position-id uint))
(let (
		(position (unwrap! (map-get? positions position-id) err-invalid-position-id))
	)
		(ok position)
	)
)

;; read only functions
(define-read-only (get-position (position-id uint))
    (map-get? positions position-id)
)

(define-read-only (get-locked-collateral-balance)
	(let (
		(total (fold sum-position-locked-balances position-ids u0))
	) 
	(ok total)
	)
)

(define-read-only (get-vault-state)
	(var-get current-vault-state)
)

;; get position state
(define-read-only (get-position-state (position-id uint))
	(let (
		(position (unwrap! (map-get? positions position-id) err-invalid-position-id))
	)
		(ok (get state position))
	)
)
;;

;; private functions
(define-private (transfer-ft (token-contract <ft-trait>) (amount uint) (sender principal) (recipient principal))
	(contract-call? token-contract transfer amount sender recipient none)
)

(define-private (balance-ft (token-contract <ft-trait>) (who principal))
	(contract-call? token-contract get-balance who)
)

(define-private (sum-position-locked-balances (addend uint) (sum uint))
	(let (
		(position (default-to dummy-position (map-get? positions addend)))
	)
		(+ (get collateralBalance position) sum)
	)
)

(define-private (common-add-remove-position-checks (position (tuple (borrower principal) (collateralBalance uint) (state uint) (id uint))))
	(begin 
		;; ownership checks
		(asserts! (is-eq tx-sender contract-owner) err-owner-only) ;; only owner can call

		;; vault checks
		(asserts! (is-eq (var-get current-vault-state) vault-state-open) err-invalid-vault-state) ;; vault state must be open

		;; common position checks
		(asserts! (is-eq (get state position) collateral-state-open) err-invalid-collateral-state)
		(asserts! (is-eq tx-sender (get borrower position)) err-owner-only) ;; borrower must match position principal
		(asserts! (is-some (some position)) err-invalid-position-id) ;; position must exist
		(asserts! (is-eq true (<= (get id position) (var-get last-position-id))) err-invalid-position-id) ;; position must be within valid range
		(ok true)
	)
)



Functions (20)

FunctionAccessArgs
depositpublicamount: uint
withdrawpublicamount: uint
create-positionpubliccollateral-amount: uint, lender: principal
add-to-positionpublicposition-id: uint, collateral-amount: uint
remove-from-positionpublicposition-id: uint, collateral-amount: uint
change-vault-statepublicnew-state: uint
lock-positionpublicposition-id: uint
liquidate-positionpublicposition-id: uint, liquidator: principal, proofTxId: (string-ascii 66
unlock-positionpublicposition-id: uint, proofTxId: (string-ascii 66
pay-liquidatorpublicposition-id: uint
get-available-collateral-balancepublic
get-borrower-positionpublicposition-id: uint
get-positionread-onlyposition-id: uint
get-locked-collateral-balanceread-only
get-vault-stateread-only
get-position-stateread-onlyposition-id: uint
transfer-ftprivatetoken-contract: <ft-trait>, amount: uint, sender: principal, recipient: principal
balance-ftprivatetoken-contract: <ft-trait>, who: principal
sum-position-locked-balancesprivateaddend: uint, sum: uint
common-add-remove-position-checksprivateposition: (tuple (borrower principal, collateralBalance: uint, state: uint, id: uint