diff --git a/Clarinet.toml b/Clarinet.toml index a64f445..f678f58 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -5,12 +5,25 @@ costs_version = 1 [contracts.token-vesting] path = "contracts/token-vesting.clar" -depends_on = ["sip-010-trait", "xyz-token"] +depends_on = ["sip-010-trait-ft-standard", "xyz-token", "arkadiko-dao"] -[contracts.sip-010-trait] -path = "contracts/sip-010-trait.clar" + +[contracts.arkadiko-dao-token-trait-v1] +depends_on = [] +path = "contracts/arkadiko/dao-token-trait-v1.clar" + +[contracts.arkadiko-token] +depends_on = ["sip-010-trait-ft-standard", "arkadiko-dao-token-trait-v1"] +path = "contracts/arkadiko/arkadiko-token.clar" + +[contracts.arkadiko-dao] +depends_on = ["arkadiko-token"] +path = "contracts/arkadiko/arkadiko-dao.clar" + +[contracts.sip-010-trait-ft-standard] depends_on = [] +path = "contracts/sip-010-trait-ft-standard.clar" [contracts.xyz-token] path = "tests/contracts/xyz-token.clar" -depends_on = ["sip-010-trait"] +depends_on = ["sip-010-trait-ft-standard"] diff --git a/contracts/arkadiko/arkadiko-dao.clar b/contracts/arkadiko/arkadiko-dao.clar new file mode 100644 index 0000000..e4fc631 --- /dev/null +++ b/contracts/arkadiko/arkadiko-dao.clar @@ -0,0 +1,429 @@ +(use-trait ft-trait .sip-010-trait-ft-standard.sip-010-trait) +(use-trait dao-token-trait .arkadiko-dao-token-trait-v1.dao-token-trait) + +;; Arkadiko DAO +;; +;; Keep contracts used in protocol. +;; Emergency switch to shut down protocol. + + +;; Errors +(define-constant ERR-NOT-AUTHORIZED u100401) + +;; Contract addresses +(define-map contracts + { name: (string-ascii 256) } + { + address: principal, ;; e.g. 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + qualified-name: principal ;; e.g. 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.freddie + } +) +(define-map contracts-data + { qualified-name: principal } + { + can-mint: bool, + can-burn: bool + } +) + +;; Variables +(define-data-var emergency-shutdown-activated bool false) +(define-data-var dao-owner principal tx-sender) +(define-data-var payout-address principal (var-get dao-owner)) ;; to which address the foundation is paid +(define-data-var guardian principal (var-get dao-owner)) ;; guardian that can be set + +(define-read-only (get-dao-owner) + (var-get dao-owner) +) + +(define-read-only (get-payout-address) + (var-get payout-address) +) + +(define-public (set-dao-owner (address principal)) + (begin + (asserts! (is-eq tx-sender (var-get dao-owner)) (err ERR-NOT-AUTHORIZED)) + + (ok (var-set dao-owner address)) + ) +) + +(define-public (set-payout-address (address principal)) + (begin + (asserts! (is-eq tx-sender (var-get dao-owner)) (err ERR-NOT-AUTHORIZED)) + + (ok (var-set payout-address address)) + ) +) + +(define-read-only (get-guardian-address) + (var-get guardian) +) + +(define-public (set-guardian-address (address principal)) + (begin + (asserts! (is-eq tx-sender (var-get guardian)) (err ERR-NOT-AUTHORIZED)) + + (ok (var-set guardian address)) + ) +) + +(define-public (toggle-emergency-shutdown) + (begin + (asserts! (is-eq tx-sender (var-get guardian)) (err ERR-NOT-AUTHORIZED)) + + (ok (var-set emergency-shutdown-activated (not (var-get emergency-shutdown-activated)))) + ) +) + +(define-read-only (get-emergency-shutdown-activated) + (ok (var-get emergency-shutdown-activated)) +) + +;; Get contract address +(define-read-only (get-contract-address-by-name (name (string-ascii 256))) + (get address (map-get? contracts { name: name })) +) + +;; Get contract qualified name +(define-read-only (get-qualified-name-by-name (name (string-ascii 256))) + (get qualified-name (map-get? contracts { name: name })) +) + +;; Check if contract can mint +(define-read-only (get-contract-can-mint-by-qualified-name (qualified-name principal)) + (default-to + false + (get can-mint (map-get? contracts-data { qualified-name: qualified-name })) + ) +) + +;; Check if contract can burn +(define-read-only (get-contract-can-burn-by-qualified-name (qualified-name principal)) + (default-to + false + (get can-burn (map-get? contracts-data { qualified-name: qualified-name })) + ) +) + +;; Governance contract can setup DAO contracts +(define-public (set-contract-address (name (string-ascii 256)) (address principal) (qualified-name principal) (can-mint bool) (can-burn bool)) + (let ( + (current-contract (map-get? contracts { name: name })) + ) + (begin + (asserts! (is-eq (unwrap-panic (get-qualified-name-by-name "governance")) contract-caller) (err ERR-NOT-AUTHORIZED)) + + (map-set contracts { name: name } { address: address, qualified-name: qualified-name }) + (if (is-some current-contract) + (map-set contracts-data { qualified-name: (unwrap-panic (get qualified-name current-contract)) } { can-mint: false, can-burn: false }) + false + ) + (map-set contracts-data { qualified-name: qualified-name } { can-mint: can-mint, can-burn: can-burn }) + (ok true) + ) + ) +) + +;; --------------------------------------------------------- +;; Protocol tokens +;; --------------------------------------------------------- + +;; Mint protocol tokens +(define-public (mint-token (token ) (amount uint) (recipient principal)) + (begin + (asserts! (is-eq (get-contract-can-mint-by-qualified-name contract-caller) true) (err ERR-NOT-AUTHORIZED)) + (print { type: "token", action: "minted", data: { amount: amount, recipient: recipient } }) + (contract-call? token mint-for-dao amount recipient) + ) +) + +;; Burn protocol tokens +(define-public (burn-token (token ) (amount uint) (recipient principal)) + (begin + (asserts! (is-eq (get-contract-can-burn-by-qualified-name contract-caller) true) (err ERR-NOT-AUTHORIZED)) + (print { type: "token", action: "burned", data: { amount: amount, recipient: recipient } }) + (contract-call? token burn-for-dao amount recipient) + ) +) + +;; This method is called by the auction engine when more bad debt needs to be burned +;; but the vault collateral is not sufficient +;; As a result, additional DIKO will be minted to cover bad debt +(define-public (request-diko-tokens (collateral-amount uint)) + (begin + (asserts! (is-eq (unwrap-panic (get-qualified-name-by-name "auction-engine")) contract-caller) (err ERR-NOT-AUTHORIZED)) + + (contract-call? .arkadiko-token mint-for-dao collateral-amount (as-contract (unwrap-panic (get-qualified-name-by-name "sip10-reserve")))) + ) +) + + +;; --------------------------------------------------------- +;; Contract initialisation +;; --------------------------------------------------------- + +;; Initialize the contract +(begin + ;; Add initial contracts + (map-set contracts + { name: "freddie" } + { + address: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM, + qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-freddie-v1-1 + } + ) + (map-set contracts-data + { qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-freddie-v1-1 } + { + can-mint: true, + can-burn: true + } + ) + + (map-set contracts + { name: "auction-engine" } + { + address: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM, + qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-auction-engine-v2-1 + } + ) + (map-set contracts-data + { qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-auction-engine-v2-1 } + { + can-mint: true, + can-burn: true + } + ) + + (map-set contracts + { name: "oracle" } + { + address: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM, + qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-oracle-v1-1 + } + ) + (map-set contracts + { name: "collateral-types" } + { + address: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM, + qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-collateral-types-v1-1 + } + ) + (map-set contracts + { name: "governance" } + { + address: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM, + qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-governance-v2-1 + } + ) + (map-set contracts + { name: "stake-registry" } + { + address: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM, + qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-stake-registry-v1-1 + } + ) + (map-set contracts-data + { qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-stake-registry-v1-1 } + { + can-mint: true, + can-burn: true + } + ) + + (map-set contracts + { name: "stake-pool-diko" } + { + address: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM, + qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-stake-pool-diko-v1-1 + } + ) + (map-set contracts-data + { qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-stake-pool-diko-v1-1 } + { + can-mint: true, + can-burn: true + } + ) + + (map-set contracts + { name: "stake-pool-diko-usda" } + { + address: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM, + qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-stake-pool-diko-usda-v1-1 + } + ) + (map-set contracts-data + { qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-stake-pool-diko-usda-v1-1 } + { + can-mint: true, + can-burn: true + } + ) + + (map-set contracts + { name: "stake-pool-wstx-usda" } + { + address: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM, + qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-stake-pool-wstx-usda-v1-1 + } + ) + (map-set contracts-data + { qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-stake-pool-wstx-usda-v1-1 } + { + can-mint: true, + can-burn: true + } + ) + + (map-set contracts + { name: "stake-pool-wstx-diko" } + { + address: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM, + qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-stake-pool-wstx-diko-v1-1 + } + ) + (map-set contracts-data + { qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-stake-pool-wstx-diko-v1-1 } + { + can-mint: true, + can-burn: true + } + ) + + (map-set contracts + { name: "stacker" } + { + address: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM, + qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-stacker-v1-1 + } + ) + (map-set contracts + { name: "stacker-2" } + { + address: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM, + qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-stacker-2-v1-1 + } + ) + (map-set contracts + { name: "stacker-3" } + { + address: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM, + qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-stacker-3-v1-1 + } + ) + (map-set contracts + { name: "stacker-4" } + { + address: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM, + qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-stacker-4-v1-1 + } + ) + + (map-set contracts + { name: "stacker-payer" } + { + address: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM, + qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-stacker-payer-v1-1 + } + ) + + (map-set contracts + { name: "stx-reserve" } + { + address: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM, + qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-stx-reserve-v1-1 + } + ) + (map-set contracts-data + { qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-stx-reserve-v1-1 } + { + can-mint: true, + can-burn: true + } + ) + + (map-set contracts + { name: "sip10-reserve" } + { + address: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM, + qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-sip10-reserve-v1-1 + } + ) + (map-set contracts-data + { qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-sip10-reserve-v1-1 } + { + can-mint: true, + can-burn: true + } + ) + + (map-set contracts + { name: "diko-guardian" } + { + address: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM, + qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-diko-guardian-v1-1 + } + ) + + (map-set contracts + { name: "diko-init" } + { + address: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM, + qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-diko-init + } + ) + (map-set contracts-data + { qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-diko-init } + { + can-mint: true, + can-burn: true + } + ) + + (map-set contracts + { name: "vault-rewards" } + { + address: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM, + qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-vault-rewards-v1-1 + } + ) + (map-set contracts-data + { qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-vault-rewards-v1-1 } + { + can-mint: true, + can-burn: true + } + ) + + (map-set contracts + { name: "swap" } + { + address: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM, + qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-swap-v2-1 + } + ) + (map-set contracts-data + { qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-swap-v2-1 } + { + can-mint: true, + can-burn: true + } + ) + + (map-set contracts + { name: "liquidator" } + { + address: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM, + qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-liquidator-v2-1 + } + ) + (map-set contracts-data + { qualified-name: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.arkadiko-liquidator-v2-1 } + { + can-mint: false, + can-burn: false + } + ) +) diff --git a/contracts/arkadiko/arkadiko-token.clar b/contracts/arkadiko/arkadiko-token.clar new file mode 100644 index 0000000..83cedf2 --- /dev/null +++ b/contracts/arkadiko/arkadiko-token.clar @@ -0,0 +1,106 @@ +(impl-trait .sip-010-trait-ft-standard.sip-010-trait) +(impl-trait .arkadiko-dao-token-trait-v1.dao-token-trait) + +;; Defines the Arkadiko Governance Token according to the SIP010 Standard +(define-fungible-token diko) + +(define-data-var token-uri (string-utf8 256) u"") +(define-data-var contract-owner principal tx-sender) + +;; errors +(define-constant ERR-NOT-AUTHORIZED u1401) + +(define-public (set-contract-owner (owner principal)) + (begin + (asserts! (is-eq tx-sender (var-get contract-owner)) (err ERR-NOT-AUTHORIZED)) + + (ok (var-set contract-owner owner)) + ) +) + +;; --------------------------------------------------------- +;; SIP-10 Functions +;; --------------------------------------------------------- + +(define-read-only (get-total-supply) + (ok (ft-get-supply diko)) +) + +(define-read-only (get-name) + (ok "Arkadiko Token") +) + +(define-read-only (get-symbol) + (ok "DIKO") +) + +(define-read-only (get-decimals) + (ok u6) +) + +(define-read-only (get-balance (account principal)) + (ok (ft-get-balance diko account)) +) + +(define-public (set-token-uri (value (string-utf8 256))) + (if (is-eq tx-sender (var-get contract-owner)) + (ok (var-set token-uri value)) + (err ERR-NOT-AUTHORIZED) + ) +) + +(define-read-only (get-token-uri) + (ok (some (var-get token-uri))) +) + +(define-public (transfer (amount uint) (sender principal) (recipient principal) (memo (optional (buff 34)))) + (begin + (asserts! (is-eq tx-sender sender) (err ERR-NOT-AUTHORIZED)) + + (match (ft-transfer? diko amount sender recipient) + response (begin + (print memo) + (ok response) + ) + error (err error) + ) + ) +) + +;; --------------------------------------------------------- +;; DAO token trait +;; --------------------------------------------------------- + +;; Mint method for DAO +(define-public (mint-for-dao (amount uint) (recipient principal)) + (begin + (asserts! (is-eq contract-caller .arkadiko-dao) (err ERR-NOT-AUTHORIZED)) + (ft-mint? diko amount recipient) + ) +) + +;; Burn method for DAO +(define-public (burn-for-dao (amount uint) (sender principal)) + (begin + (asserts! (is-eq contract-caller .arkadiko-dao) (err ERR-NOT-AUTHORIZED)) + (ft-burn? diko amount sender) + ) +) + +;; Burn external +(define-public (burn (amount uint) (sender principal)) + (begin + (asserts! (is-eq tx-sender sender) (err ERR-NOT-AUTHORIZED)) + (ft-burn? diko amount sender) + ) +) + +;; Test environments +(begin + ;; TODO: do not do this on testnet or mainnet + (try! (ft-mint? diko u890000000000 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM)) + (try! (ft-mint? diko u150000000000 'ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5)) + (try! (ft-mint? diko u150000000000 'ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG)) + (try! (ft-mint? diko u1000000000000 'STB2BWB0K5XZGS3FXVTG3TKS46CQVV66NAK3YVN8)) + (try! (ft-mint? diko u1000000000000 'ST1QV6WVNED49CR34E58CRGA0V58X281FAS1TFBWF)) +) diff --git a/contracts/arkadiko/dao-token-trait-v1.clar b/contracts/arkadiko/dao-token-trait-v1.clar new file mode 100644 index 0000000..46b42ec --- /dev/null +++ b/contracts/arkadiko/dao-token-trait-v1.clar @@ -0,0 +1,12 @@ +(define-trait dao-token-trait + ( + ;; Mint - Used by DAO + (mint-for-dao (uint principal) (response bool uint)) + + ;; Burn - Used by DAO + (burn-for-dao (uint principal) (response bool uint)) + + ;; Burn - External usage + (burn (uint principal) (response bool uint)) + ) +) diff --git a/contracts/sip-010-trait.clar b/contracts/sip-010-trait-ft-standard.clar similarity index 100% rename from contracts/sip-010-trait.clar rename to contracts/sip-010-trait-ft-standard.clar diff --git a/contracts/token-vesting.clar b/contracts/token-vesting.clar index 0a129ce..e197664 100644 --- a/contracts/token-vesting.clar +++ b/contracts/token-vesting.clar @@ -1,4 +1,3 @@ - ;; Token Vesting ;; ;; Vesting contract that allows deposit of SIP-010 tokens. @@ -6,13 +5,15 @@ ;; set of addresses at a certain schedule and conditions. ;; SIP-010 token trait. -(use-trait ft-trait .sip-010-trait.sip-010-trait) +(use-trait ft-trait .sip-010-trait-ft-standard.sip-010-trait) ;; ;; -- Error codes ;; -(define-constant SHARE-ALREADY-REDEEMED (err u10)) +(define-constant ERR-SHARE-ALREADY-REDEEMED (err u10)) +(define-constant ERR-NOT-AUTHORIZED (err u11)) +(define-constant ERR-NOT-WHITELISTED (err u12)) ;; ;; -- Data @@ -28,8 +29,14 @@ ;; Shares data structure. ;; Stores vestings shares. (define-map shares - (tuple (address principal) (token principal)) - (tuple (amount uint) (redeemed bool)) + { address: principal, token: principal } + { amount: uint, redeemed: bool } +) + +;; Keeps all the token contracts that can be used in the vesting +;; A new contract can be added by Arkadiko DAO +(define-map whitelisted-tokens + { contract: principal } { whitelisted: bool } ) ;; Current token context. @@ -46,12 +53,15 @@ (locking-period uint) (assignees (list 10 (tuple (address principal) (amount uint)))) ) - (begin + (let ( + (whitelisted-token (get-whitelisted-token (contract-of token))) + ) + (asserts! (get whitelisted whitelisted-token) ERR-NOT-WHITELISTED) + (add-to-vestings token amount locking-period) (var-set token-context (some (contract-of token))) (map add-to-shares assignees) - (try! (contract-call? token transfer - amount tx-sender (as-contract tx-sender) none)) + (try! (contract-call? token transfer amount tx-sender (as-contract tx-sender) none)) (ok true) ) ) @@ -61,15 +71,28 @@ (define-public (redeem (token )) (let ( (recipient contract-caller) - (share (get-share {address: tx-sender, token: (contract-of token)}))) - (asserts! (not (get redeemed share)) SHARE-ALREADY-REDEEMED) - (unwrap-panic (as-contract (contract-call? token transfer - (get amount share) tx-sender recipient none))) + (share (get-share {address: tx-sender, token: (contract-of token)})) + (whitelisted-token (get-whitelisted-token (contract-of token))) + ) + (asserts! (not (get redeemed share)) ERR-SHARE-ALREADY-REDEEMED) + (asserts! (get whitelisted whitelisted-token) ERR-NOT-WHITELISTED) + + (unwrap-panic (as-contract (contract-call? token transfer (get amount share) tx-sender recipient none))) (mark-share-as-redeemed token) (ok true) ) ) +;; Adds a token to the whitelist +;; Currently can only be called by the Arkadiko DAO +(define-public (whitelist-token (token )) + (begin + (asserts! (is-eq tx-sender (contract-call? .arkadiko-dao get-dao-owner)) ERR-NOT-AUTHORIZED) + (map-set whitelisted-tokens { contract: (contract-of token) } { whitelisted: true }) + (ok true) + ) +) + ;; ;; -- Private ;; @@ -87,20 +110,35 @@ ;; Add a deposit to the vestings storage. (define-private (add-to-vestings (token ) (amount uint) (locking-period uint)) (map-set vestings - {depositor: tx-sender, token: (contract-of token)} - {amount: amount, locking-period: locking-period} + { depositor: tx-sender, token: (contract-of token) } + { amount: amount, locking-period: locking-period } ) ) ;; Add a share to the shares storage. (define-private (add-to-shares (share (tuple (address principal) (amount uint)))) (map-set shares - {address: (get address share), token: (unwrap-panic (var-get token-context))} - {amount: (get amount share), redeemed: false} + { address: (get address share), token: (unwrap-panic (var-get token-context)) } + { amount: (get amount share), redeemed: false } ) ) ;; Get amount in the shares storage. (define-private (get-share (key (tuple (address principal) (token principal)))) - (unwrap-panic (map-get? shares key)) + (default-to + { + amount: u0, + redeemed: true + } + (map-get? shares key) + ) +) + +(define-private (get-whitelisted-token (token principal)) + (default-to + { + whitelisted: false + } + (map-get? whitelisted-tokens { contract: token }) + ) ) diff --git a/tests/contracts/xyz-token.clar b/tests/contracts/xyz-token.clar index e39406b..1d68c1c 100644 --- a/tests/contracts/xyz-token.clar +++ b/tests/contracts/xyz-token.clar @@ -1,5 +1,5 @@ ;; Implement the `ft-trait` trait defined in the `ft-trait` contract -(impl-trait .sip-010-trait.sip-010-trait) +(impl-trait .sip-010-trait-ft-standard.sip-010-trait) (define-fungible-token xyz-token) diff --git a/tests/token-vesting_test.ts b/tests/token-vesting_test.ts index 8d0d1a0..370b12f 100644 --- a/tests/token-vesting_test.ts +++ b/tests/token-vesting_test.ts @@ -1,13 +1,13 @@ import { - Clarinet, - Tx, - Chain, - Account, - types + Clarinet, + Tx, + Chain, + Account, + types } from 'https://deno.land/x/clarinet@v0.14.0/index.ts'; import { - assertEquals + assertEquals } from 'https://deno.land/std@0.90.0/testing/asserts.ts'; const contract = "token-vesting"; @@ -15,166 +15,231 @@ const contract = "token-vesting"; /* * Vesting options: locking 2000 XYZ tokens for 48 blocks. */ -const vestingOptions = { - token: "xyz-token", - lockingPeriod: 48, - amount: 2000 + const vestingOptions = { + token: "xyz-token", + lockingPeriod: 48, + amount: 2000 } class XyzToken { - chain: Chain; - deployer: Account; + chain: Chain; + deployer: Account; - constructor(chain: Chain, deployer: Account) { - this.chain = chain; - this.deployer = deployer; - } + constructor(chain: Chain, deployer: Account) { + this.chain = chain; + this.deployer = deployer; + } - balanceOf = (wallet: string) => { - const block = this.chain.callReadOnlyFn(vestingOptions.token, "get-balance", [ - types.principal(wallet), - ], this.deployer.address); + balanceOf = (wallet: string) => { + const block = this.chain.callReadOnlyFn(vestingOptions.token, "get-balance", [ + types.principal(wallet), + ], this.deployer.address); - return block; - } + return block; + } } Clarinet.test({ - name: "[deposit] locked amount is transferred to the contract address", - async fn(chain: Chain, accounts: Map) { - const deployer = accounts.get("deployer")!; - const assignees = [{address: accounts.get("wallet_3")!.address, amount: 25}]; - let assigneesList: any[] = []; - assignees.forEach((el) => { - assigneesList.push( - types.tuple({ - 'address': types.principal(el.address), - 'amount': types.uint(el.amount) - }) - ) - }); - - const block = chain.mineBlock( - [ - Tx.contractCall( - contract, "deposit", - [ - types.principal(`${deployer.address}.${vestingOptions.token}`), - types.uint(vestingOptions.amount), - types.uint(vestingOptions.lockingPeriod), - types.list(assigneesList) - ], - deployer.address - ), - ] - ); - - let resp = block.receipts[0]; - resp.result.expectOk().expectBool(true); - - // Check balance in the contract address. - const xyzToken = new XyzToken(chain, deployer); - resp = xyzToken.balanceOf(`${deployer.address}.${contract}`); - resp.result.expectOk().expectUint(vestingOptions.amount); - } + name: "[deposit] when token is not whitelisted. Vesting is not allowed", + async fn(chain: Chain, accounts: Map) { + const deployer = accounts.get("deployer")!; + const assignees = [{address: accounts.get("wallet_3")!.address, amount: 25}]; + let assigneesList: any[] = []; + assignees.forEach((el) => { + assigneesList.push( + types.tuple({ + 'address': types.principal(el.address), + 'amount': types.uint(el.amount) + }) + ) + }); + + const block = chain.mineBlock( + [ + Tx.contractCall( + contract, "deposit", + [ + types.principal(`${deployer.address}.${vestingOptions.token}`), + types.uint(vestingOptions.amount), + types.uint(vestingOptions.lockingPeriod), + types.list(assigneesList) + ], + deployer.address + ), + ] + ); + + const resp = block.receipts[0]; + resp.result.expectErr().expectUint(12); + } }); Clarinet.test({ - name: "[redeem] unlocked amount is transferred to the transaction sender address", - async fn(chain: Chain, accounts: Map) { - const deployer = accounts.get("deployer")!; - const txSender = accounts.get("wallet_2")!; - const assignees = [{address: txSender.address, amount: 250}]; - let assigneesList: any[] = []; - assignees.forEach((el) => { - assigneesList.push( - types.tuple({ - 'address': types.principal(el.address), - 'amount': types.uint(el.amount) - }) - ) - }); - - const block = chain.mineBlock( - [ - Tx.contractCall( - contract, "deposit", - [ - types.principal(`${deployer.address}.${vestingOptions.token}`), - types.uint(vestingOptions.amount), - types.uint(vestingOptions.lockingPeriod), - types.list(assigneesList) - ], - deployer.address - ), - Tx.contractCall( - contract, "redeem", - [types.principal(`${deployer.address}.${vestingOptions.token}`)], - txSender.address - ) - ] - ); - - const [deposit, redeem] = block.receipts; - deposit.result.expectOk(); - redeem.result.expectOk(); - - // Check balance in the assignee address. - const xyzToken = new XyzToken(chain, deployer); - const result = xyzToken.balanceOf(txSender.address).result; - result.expectOk().expectUint(assignees[0].amount); - } + name: "[deposit] when token is whitelisted. Locked amount is transferred to the contract address", + async fn(chain: Chain, accounts: Map) { + const deployer = accounts.get("deployer")!; + const assignees = [{address: accounts.get("wallet_3")!.address, amount: 25}]; + let assigneesList: any[] = []; + assignees.forEach((el) => { + assigneesList.push( + types.tuple({ + 'address': types.principal(el.address), + 'amount': types.uint(el.amount) + }) + ) + }); + + const block = chain.mineBlock( + [ + Tx.contractCall(contract, "whitelist-token", [types.principal(`${deployer.address}.${vestingOptions.token}`)], deployer.address), + Tx.contractCall( + contract, "deposit", + [ + types.principal(`${deployer.address}.${vestingOptions.token}`), + types.uint(vestingOptions.amount), + types.uint(vestingOptions.lockingPeriod), + types.list(assigneesList) + ], + deployer.address + ), + ] + ); + + let resp = block.receipts[0]; + resp.result.expectOk().expectBool(true); + + // Check balance in the contract address. + const xyzToken = new XyzToken(chain, deployer); + resp = xyzToken.balanceOf(`${deployer.address}.${contract}`); + resp.result.expectOk().expectUint(vestingOptions.amount); + } }); Clarinet.test({ - name: "[redeem] shares can be redeemed only once", - async fn(chain: Chain, accounts: Map) { - const deployer = accounts.get("deployer")!; - const txSender = accounts.get("wallet_2")!; - const assignees = [{address: txSender.address, amount: 250}]; - let assigneesList: any[] = []; - assignees.forEach((el) => { - assigneesList.push( - types.tuple({ - 'address': types.principal(el.address), - 'amount': types.uint(el.amount) - }) - ) - }); - - const block = chain.mineBlock( - [ - Tx.contractCall( - contract, "deposit", - [ - types.principal(`${deployer.address}.${vestingOptions.token}`), - types.uint(vestingOptions.amount), - types.uint(vestingOptions.lockingPeriod), - types.list(assigneesList) - ], - deployer.address - ), - Tx.contractCall( - contract, "redeem", - [types.principal(`${deployer.address}.${vestingOptions.token}`)], - txSender.address - ), - Tx.contractCall( - contract, "redeem", - [types.principal(`${deployer.address}.${vestingOptions.token}`)], - txSender.address - ) - ] - ); - - const [deposit, redeem, notRedeemed] = block.receipts; - deposit.result.expectOk(); - redeem.result.expectOk(); - notRedeemed.result.expectErr().expectUint(10); - - // Check the balance in the assignee address. - const xyzToken = new XyzToken(chain, deployer); - const result = xyzToken.balanceOf(txSender.address).result; - result.expectOk().expectUint(assignees[0].amount); - } + name: "[redeem] unlocked amount is transferred to the transaction sender address", + async fn(chain: Chain, accounts: Map) { + const deployer = accounts.get("deployer")!; + const txSender = accounts.get("wallet_2")!; + const assignees = [{address: txSender.address, amount: 250}]; + let assigneesList: any[] = []; + assignees.forEach((el) => { + assigneesList.push( + types.tuple({ + 'address': types.principal(el.address), + 'amount': types.uint(el.amount) + }) + ) + }); + + let block = chain.mineBlock( + [ + Tx.contractCall( + contract, "deposit", + [ + types.principal(`${deployer.address}.${vestingOptions.token}`), + types.uint(vestingOptions.amount), + types.uint(vestingOptions.lockingPeriod), + types.list(assigneesList) + ], + deployer.address + ), + Tx.contractCall( + contract, "redeem", + [types.principal(`${deployer.address}.${vestingOptions.token}`)], + txSender.address + ) + ] + ); + + const [deposit, redeem] = block.receipts; + deposit.result.expectErr(); + redeem.result.expectErr(); + + // Check balance in the assignee address. + const xyzToken = new XyzToken(chain, deployer); + let result = xyzToken.balanceOf(txSender.address).result; + result.expectOk().expectUint(0); + + block = chain.mineBlock( + [ + Tx.contractCall(contract, "whitelist-token", [types.principal(`${deployer.address}.${vestingOptions.token}`)], deployer.address), + Tx.contractCall( + contract, "deposit", + [ + types.principal(`${deployer.address}.${vestingOptions.token}`), + types.uint(vestingOptions.amount), + types.uint(vestingOptions.lockingPeriod), + types.list(assigneesList) + ], + deployer.address + ), + Tx.contractCall( + contract, "redeem", + [types.principal(`${deployer.address}.${vestingOptions.token}`)], + txSender.address + ) + ] + ); + const [deposit2, redeem2] = block.receipts; + deposit2.result.expectOk(); + redeem2.result.expectOk(); + + // Check balance in the assignee address. + result = xyzToken.balanceOf(txSender.address).result; + result.expectOk().expectUint(250); + } +}); + +Clarinet.test({ + name: "[redeem] shares can be redeemed only once", + async fn(chain: Chain, accounts: Map) { + const deployer = accounts.get("deployer")!; + const txSender = accounts.get("wallet_2")!; + const assignees = [{address: txSender.address, amount: 250}]; + let assigneesList: any[] = []; + assignees.forEach((el) => { + assigneesList.push( + types.tuple({ + 'address': types.principal(el.address), + 'amount': types.uint(el.amount) + }) + ) + }); + + const block = chain.mineBlock( + [ + Tx.contractCall(contract, "whitelist-token", [types.principal(`${deployer.address}.${vestingOptions.token}`)], deployer.address), + Tx.contractCall( + contract, "deposit", + [ + types.principal(`${deployer.address}.${vestingOptions.token}`), + types.uint(vestingOptions.amount), + types.uint(vestingOptions.lockingPeriod), + types.list(assigneesList) + ], + deployer.address + ), + Tx.contractCall( + contract, "redeem", + [types.principal(`${deployer.address}.${vestingOptions.token}`)], + txSender.address + ), + Tx.contractCall( + contract, "redeem", + [types.principal(`${deployer.address}.${vestingOptions.token}`)], + txSender.address + ) + ] + ); + + const [deposit, redeem, notRedeemed] = block.receipts; + deposit.result.expectOk(); + redeem.result.expectOk(); + notRedeemed.result.expectOk().expectBool(true); + + // Check the balance in the assignee address. + const xyzToken = new XyzToken(chain, deployer); + const result = xyzToken.balanceOf(txSender.address).result; + result.expectOk().expectUint(assignees[0].amount); + } });