From f6ed0a73382defb118976b6fb93d7c4d2aff1467 Mon Sep 17 00:00:00 2001 From: Amobi Ndubuisi Date: Tue, 17 Sep 2024 06:38:55 -0700 Subject: [PATCH 1/2] Added Readme file and fixed all errors in the smart contract --- securelock/.gitignore | 4 + securelock/.vscode/settings.json | 4 + securelock/.vscode/tasks.json | 18 +++ securelock/Clarinet.toml | 22 ++++ securelock/README.md | 84 ++++++++++++++ securelock/contracts/tokensafe.clar | 170 ++++++++++++++++++++++++++++ securelock/settings/Devnet.toml | 127 +++++++++++++++++++++ securelock/tests/tokensafe_test.ts | 26 +++++ 8 files changed, 455 insertions(+) create mode 100644 securelock/.gitignore create mode 100644 securelock/.vscode/settings.json create mode 100644 securelock/.vscode/tasks.json create mode 100644 securelock/Clarinet.toml create mode 100644 securelock/README.md create mode 100644 securelock/contracts/tokensafe.clar create mode 100644 securelock/settings/Devnet.toml create mode 100644 securelock/tests/tokensafe_test.ts diff --git a/securelock/.gitignore b/securelock/.gitignore new file mode 100644 index 0000000..f18b582 --- /dev/null +++ b/securelock/.gitignore @@ -0,0 +1,4 @@ + +settings/Mainnet.toml +settings/Testnet.toml +history.txt diff --git a/securelock/.vscode/settings.json b/securelock/.vscode/settings.json new file mode 100644 index 0000000..02e21eb --- /dev/null +++ b/securelock/.vscode/settings.json @@ -0,0 +1,4 @@ + +{ + "deno.enable": true, +} diff --git a/securelock/.vscode/tasks.json b/securelock/.vscode/tasks.json new file mode 100644 index 0000000..22af91c --- /dev/null +++ b/securelock/.vscode/tasks.json @@ -0,0 +1,18 @@ + +{ + "version": "2.0.0", + "tasks": [ + { + "label": "check contracts", + "group": "test", + "type": "shell", + "command": "clarinet check" + }, + { + "label": "test contracts", + "group": "test", + "type": "shell", + "command": "clarinet test" + } + ] +} diff --git a/securelock/Clarinet.toml b/securelock/Clarinet.toml new file mode 100644 index 0000000..b733226 --- /dev/null +++ b/securelock/Clarinet.toml @@ -0,0 +1,22 @@ +[project] +name = "securelock" +authors = [] +description = "" +telemetry = true +requirements = [] +[contracts.tokensafe] +path = "contracts/tokensafe.clar" +depends_on = [] + +[repl] +costs_version = 2 +parser_version = 2 + +[repl.analysis] +passes = ["check_checker"] + +[repl.analysis.check_checker] +strict = false +trusted_sender = false +trusted_caller = false +callee_filter = false diff --git a/securelock/README.md b/securelock/README.md new file mode 100644 index 0000000..5fe7f08 --- /dev/null +++ b/securelock/README.md @@ -0,0 +1,84 @@ +# Vesting Smart Contract + +## Overview + +This Clarity smart contract implements a token vesting system designed for securing team or advisor tokens, ensuring long-term commitment, and managing vesting distribution over time to various participants. The contract allows for the creation of customized vesting schedules, token claiming, and transfers of vested tokens. + +## Features + +- Token initialization with custom name, symbol, and decimals +- Creation of personalized vesting schedules for participants +- Vesting period with customizable start time, cliff period, and vesting duration +- Calculation of vested tokens based on the current block height +- Claiming of vested tokens by participants +- Transfer of claimed tokens between participants +- Querying of token balances, vesting schedules, and contract information + +## Contract Structure + +The smart contract consists of the following main components: + +1. Data Variables: + - Token information (name, symbol, decimals) + - Total supply + - Contract initialization status + +2. Data Maps: + - Token balances + - Vesting schedules + +3. Public Functions: + - `initialize`: Set up the token with basic information + - `create-vesting-schedule`: Create a vesting schedule for a participant + - `claim-vested-tokens`: Allow participants to claim their vested tokens + - `transfer`: Enable transferring of claimed tokens + +4. Read-Only Functions: + - `get-vested-amount`: Calculate the amount of tokens vested for a participant + - `get-balance`: Retrieve the token balance of an account + - `get-total-supply`: Get the total token supply + - `get-name`, `get-symbol`, `get-decimals`: Retrieve token information + - `get-vesting-schedule`: Get the vesting schedule for a participant + - `is-initialized`: Check if the contract has been initialized + +## Setup and Deployment + +To set up and deploy this smart contract: + +1. Ensure you have [Clarinet](https://github.com/hirosystems/clarinet) installed. +2. Create a new Clarinet project: `clarinet new vesting-contract` +3. Replace the contents of `contracts/vesting-contract.clar` with the provided smart contract code. +4. Update the `Clarinet.toml` file to include the contract: + + ```toml + [contracts.vesting-contract] + path = "contracts/vesting-contract.clar" + clarity_version = 2 + epoch = 2.4 + ``` + +5. Test the contract locally: `clarinet test` +6. Deploy the contract to a testnet or mainnet using Clarinet or other Stacks deployment tools. + +## Security Considerations + +- Only the contract owner can initialize the contract and create vesting schedules. +- Participants can only claim tokens that have vested according to their schedule. +- The contract uses various checks and balances to prevent unauthorized actions and ensure the integrity of the vesting process. + +## Testing + +Comprehensive unit tests should be written to cover all functions and edge cases. Use Clarinet's testing framework to write and run tests: + + +## Limitations and Future Improvements + +- The contract doesn't support token minting or burning. +- There's no functionality to modify or revoke vesting schedules once created. +- Consider adding events (using `print`) to log important actions for off-chain tracking. +- Implement a function to allow participants to check their claimable token amount directly. + + +## Author + +[Amobi Ndubuisi or dev.triggerfish@gmail.com] diff --git a/securelock/contracts/tokensafe.clar b/securelock/contracts/tokensafe.clar new file mode 100644 index 0000000..26c8e19 --- /dev/null +++ b/securelock/contracts/tokensafe.clar @@ -0,0 +1,170 @@ +;; vesting-contract.clar + +;; Define constants +(define-constant CONTRACT_OWNER tx-sender) +(define-constant ERR_UNAUTHORIZED (err u100)) +(define-constant ERR_ALREADY_INITIALIZED (err u101)) +(define-constant ERR_NOT_INITIALIZED (err u102)) +(define-constant ERR_NO_VESTING_SCHEDULE (err u103)) +(define-constant ERR_INSUFFICIENT_BALANCE (err u104)) +(define-constant ERR_INVALID_PARAMETER (err u105)) +(define-constant ERR_TRANSFER_FAILED (err u106)) + +;; Define data variables +(define-data-var token-name (string-ascii 32) "") +(define-data-var token-symbol (string-ascii 32) "") +(define-data-var token-decimals uint u0) +(define-data-var total-supply uint u0) +(define-data-var contract-initialized bool false) + +;; Define data maps +(define-map token-balances principal uint) +(define-map vesting-schedules + principal + { + total-allocation: uint, + start-block: uint, + cliff-duration: uint, + vesting-duration: uint, + vesting-interval: uint, + amount-claimed: uint + } +) + +;; Initialize the contract +(define-public (initialize (name (string-ascii 32)) (symbol (string-ascii 32)) (decimals uint)) + (begin + (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_UNAUTHORIZED) + (asserts! (not (var-get contract-initialized)) ERR_ALREADY_INITIALIZED) + (asserts! (and (> (len name) u0) (<= (len name) u32)) ERR_INVALID_PARAMETER) + (asserts! (and (> (len symbol) u0) (<= (len symbol) u32)) ERR_INVALID_PARAMETER) + (asserts! (<= decimals u18) ERR_INVALID_PARAMETER) + (var-set token-name name) + (var-set token-symbol symbol) + (var-set token-decimals decimals) + (var-set contract-initialized true) + (ok true) + ) +) + +;; Create a vesting schedule for a participant +(define-public (create-vesting-schedule + (participant principal) + (total-allocation uint) + (start-block uint) + (cliff-duration uint) + (vesting-duration uint) + (vesting-interval uint) +) + (begin + (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_UNAUTHORIZED) + (asserts! (var-get contract-initialized) ERR_NOT_INITIALIZED) + (asserts! (> total-allocation u0) ERR_INVALID_PARAMETER) + (asserts! (>= start-block block-height) ERR_INVALID_PARAMETER) + (asserts! (>= vesting-duration cliff-duration) ERR_INVALID_PARAMETER) + (asserts! (> vesting-interval u0) ERR_INVALID_PARAMETER) + (asserts! (<= vesting-interval vesting-duration) ERR_INVALID_PARAMETER) + (map-set vesting-schedules participant { + total-allocation: total-allocation, + start-block: start-block, + cliff-duration: cliff-duration, + vesting-duration: vesting-duration, + vesting-interval: vesting-interval, + amount-claimed: u0 + }) + (var-set total-supply (+ (var-get total-supply) total-allocation)) + (ok true) + ) +) + +;; Calculate vested amount for a participant +(define-read-only (get-vested-amount (participant principal)) + (let ( + (schedule (unwrap! (map-get? vesting-schedules participant) ERR_NO_VESTING_SCHEDULE)) + (current-block block-height) + (vesting-start (+ (get start-block schedule) (get cliff-duration schedule))) + (vesting-end (+ (get start-block schedule) (get vesting-duration schedule))) + ) + (if (>= current-block vesting-end) + (ok (get total-allocation schedule)) + (if (< current-block vesting-start) + (ok u0) + (let ( + (vested-periods (/ (- current-block vesting-start) (get vesting-interval schedule))) + (vesting-ratio (/ (* vested-periods (get vesting-interval schedule)) (get vesting-duration schedule))) + ) + (ok (/ (* (get total-allocation schedule) vesting-ratio) u100)) + ) + ) + ) + ) +) + +;; Claim vested tokens +(define-public (claim-vested-tokens) + (let ( + (participant tx-sender) + (schedule (unwrap! (map-get? vesting-schedules participant) ERR_NO_VESTING_SCHEDULE)) + (vested-amount (unwrap! (get-vested-amount participant) ERR_INVALID_PARAMETER)) + (claimable-amount (- vested-amount (get amount-claimed schedule))) + ) + (asserts! (> claimable-amount u0) ERR_INSUFFICIENT_BALANCE) + (map-set vesting-schedules participant + (merge schedule { amount-claimed: vested-amount }) + ) + (map-set token-balances participant + (+ (default-to u0 (map-get? token-balances participant)) claimable-amount) + ) + (ok claimable-amount) + ) +) + +;; Get balance of a participant +(define-read-only (get-balance (account principal)) + (ok (default-to u0 (map-get? token-balances account))) +) + +;; Transfer tokens (only for claimed tokens) +(define-public (transfer (amount uint) (recipient principal)) + (let ( + (sender tx-sender) + (sender-balance (unwrap! (get-balance sender) ERR_INVALID_PARAMETER)) + ) + (asserts! (>= sender-balance amount) ERR_INSUFFICIENT_BALANCE) + (map-set token-balances sender (- sender-balance amount)) + (map-set token-balances recipient + (+ (default-to u0 (map-get? token-balances recipient)) amount) + ) + (ok true) + ) +) + +;; Get total supply +(define-read-only (get-total-supply) + (ok (var-get total-supply)) +) + +;; Get token name +(define-read-only (get-name) + (ok (var-get token-name)) +) + +;; Get token symbol +(define-read-only (get-symbol) + (ok (var-get token-symbol)) +) + +;; Get token decimals +(define-read-only (get-decimals) + (ok (var-get token-decimals)) +) + +;; Get vesting schedule for a participant +(define-read-only (get-vesting-schedule (participant principal)) + (ok (map-get? vesting-schedules participant)) +) + +;; Check if the contract is initialized +(define-read-only (is-initialized) + (ok (var-get contract-initialized)) +) \ No newline at end of file diff --git a/securelock/settings/Devnet.toml b/securelock/settings/Devnet.toml new file mode 100644 index 0000000..8a5ff75 --- /dev/null +++ b/securelock/settings/Devnet.toml @@ -0,0 +1,127 @@ +[network] +name = "devnet" +deployment_fee_rate = 10 + +[accounts.deployer] +mnemonic = "twice kind fence tip hidden tilt action fragile skin nothing glory cousin green tomorrow spring wrist shed math olympic multiply hip blue scout claw" +balance = 100_000_000_000_000 +# secret_key: 753b7cc01a1a2e86221266a154af739463fce51219d97e4f856cd7200c3bd2a601 +# stx_address: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM +# btc_address: mqVnk6NPRdhntvfm4hh9vvjiRkFDUuSYsH + +[accounts.wallet_1] +mnemonic = "sell invite acquire kitten bamboo drastic jelly vivid peace spawn twice guilt pave pen trash pretty park cube fragile unaware remain midnight betray rebuild" +balance = 100_000_000_000_000 +# secret_key: 7287ba251d44a4d3fd9276c88ce34c5c52a038955511cccaf77e61068649c17801 +# stx_address: ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5 +# btc_address: mr1iPkD9N3RJZZxXRk7xF9d36gffa6exNC + +[accounts.wallet_2] +mnemonic = "hold excess usual excess ring elephant install account glad dry fragile donkey gaze humble truck breeze nation gasp vacuum limb head keep delay hospital" +balance = 100_000_000_000_000 +# secret_key: 530d9f61984c888536871c6573073bdfc0058896dc1adfe9a6a10dfacadc209101 +# stx_address: ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG +# btc_address: muYdXKmX9bByAueDe6KFfHd5Ff1gdN9ErG + +[accounts.wallet_3] +mnemonic = "cycle puppy glare enroll cost improve round trend wrist mushroom scorpion tower claim oppose clever elephant dinosaur eight problem before frozen dune wagon high" +balance = 100_000_000_000_000 +# secret_key: d655b2523bcd65e34889725c73064feb17ceb796831c0e111ba1a552b0f31b3901 +# stx_address: ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC +# btc_address: mvZtbibDAAA3WLpY7zXXFqRa3T4XSknBX7 + +[accounts.wallet_4] +mnemonic = "board list obtain sugar hour worth raven scout denial thunder horse logic fury scorpion fold genuine phrase wealth news aim below celery when cabin" +balance = 100_000_000_000_000 +# secret_key: f9d7206a47f14d2870c163ebab4bf3e70d18f5d14ce1031f3902fbbc894fe4c701 +# stx_address: ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND +# btc_address: mg1C76bNTutiCDV3t9nWhZs3Dc8LzUufj8 + +[accounts.wallet_5] +mnemonic = "hurry aunt blame peanut heavy update captain human rice crime juice adult scale device promote vast project quiz unit note reform update climb purchase" +balance = 100_000_000_000_000 +# secret_key: 3eccc5dac8056590432db6a35d52b9896876a3d5cbdea53b72400bc9c2099fe801 +# stx_address: ST2REHHS5J3CERCRBEPMGH7921Q6PYKAADT7JP2VB +# btc_address: mweN5WVqadScHdA81aATSdcVr4B6dNokqx + +[accounts.wallet_6] +mnemonic = "area desk dutch sign gold cricket dawn toward giggle vibrant indoor bench warfare wagon number tiny universe sand talk dilemma pottery bone trap buddy" +balance = 100_000_000_000_000 +# secret_key: 7036b29cb5e235e5fd9b09ae3e8eec4404e44906814d5d01cbca968a60ed4bfb01 +# stx_address: ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0 +# btc_address: mzxXgV6e4BZSsz8zVHm3TmqbECt7mbuErt + +[accounts.wallet_7] +mnemonic = "prevent gallery kind limb income control noise together echo rival record wedding sense uncover school version force bleak nuclear include danger skirt enact arrow" +balance = 100_000_000_000_000 +# secret_key: b463f0df6c05d2f156393eee73f8016c5372caa0e9e29a901bb7171d90dc4f1401 +# stx_address: ST3PF13W7Z0RRM42A8VZRVFQ75SV1K26RXEP8YGKJ +# btc_address: n37mwmru2oaVosgfuvzBwgV2ysCQRrLko7 + +[accounts.wallet_8] +mnemonic = "female adjust gallery certain visit token during great side clown fitness like hurt clip knife warm bench start reunion globe detail dream depend fortune" +balance = 100_000_000_000_000 +# secret_key: 6a1a754ba863d7bab14adbbc3f8ebb090af9e871ace621d3e5ab634e1422885e01 +# stx_address: ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP +# btc_address: n2v875jbJ4RjBnTjgbfikDfnwsDV5iUByw + +[accounts.wallet_9] +mnemonic = "shadow private easily thought say logic fault paddle word top book during ignore notable orange flight clock image wealth health outside kitten belt reform" +balance = 100_000_000_000_000 +# secret_key: de433bdfa14ec43aa1098d5be594c8ffb20a31485ff9de2923b2689471c401b801 +# stx_address: STNHKEPYEPJ8ET55ZZ0M5A34J0R3N5FM2CMMMAZ6 +# btc_address: mjSrB3wS4xab3kYqFktwBzfTdPg367ZJ2d + +[devnet] +disable_bitcoin_explorer = true +# disable_stacks_explorer = true +# disable_stacks_api = true +# working_dir = "tmp/devnet" +# stacks_node_events_observers = ["host.docker.internal:8002"] +# miner_mnemonic = "twice kind fence tip hidden tilt action fragile skin nothing glory cousin green tomorrow spring wrist shed math olympic multiply hip blue scout claw" +# miner_derivation_path = "m/44'/5757'/0'/0/0" +# orchestrator_port = 20445 +# bitcoin_node_p2p_port = 18444 +# bitcoin_node_rpc_port = 18443 +# bitcoin_node_username = "devnet" +# bitcoin_node_password = "devnet" +# bitcoin_controller_port = 18442 +# bitcoin_controller_block_time = 30_000 +# stacks_node_rpc_port = 20443 +# stacks_node_p2p_port = 20444 +# stacks_api_port = 3999 +# stacks_api_events_port = 3700 +# bitcoin_explorer_port = 8001 +# stacks_explorer_port = 8000 +# postgres_port = 5432 +# postgres_username = "postgres" +# postgres_password = "postgres" +# postgres_database = "postgres" +# bitcoin_node_image_url = "quay.io/hirosystems/bitcoind:devnet" +# stacks_node_image_url = "localhost:5000/stacks-node:devnet" +# stacks_api_image_url = "blockstack/stacks-blockchain-api:latest" +# stacks_explorer_image_url = "blockstack/explorer:latest" +# bitcoin_explorer_image_url = "quay.io/hirosystems/bitcoin-explorer:devnet" +# postgres_image_url = "postgres:alpine" + +# Send some stacking orders +[[devnet.pox_stacking_orders]] +start_at_cycle = 3 +duration = 12 +wallet = "wallet_1" +slots = 2 +btc_address = "mr1iPkD9N3RJZZxXRk7xF9d36gffa6exNC" + +[[devnet.pox_stacking_orders]] +start_at_cycle = 3 +duration = 12 +wallet = "wallet_2" +slots = 1 +btc_address = "muYdXKmX9bByAueDe6KFfHd5Ff1gdN9ErG" + +[[devnet.pox_stacking_orders]] +start_at_cycle = 3 +duration = 12 +wallet = "wallet_3" +slots = 1 +btc_address = "mvZtbibDAAA3WLpY7zXXFqRa3T4XSknBX7" diff --git a/securelock/tests/tokensafe_test.ts b/securelock/tests/tokensafe_test.ts new file mode 100644 index 0000000..9a18ae0 --- /dev/null +++ b/securelock/tests/tokensafe_test.ts @@ -0,0 +1,26 @@ + +import { Clarinet, Tx, Chain, Account, types } from 'https://deno.land/x/clarinet@v0.14.0/index.ts'; +import { assertEquals } from 'https://deno.land/std@0.90.0/testing/asserts.ts'; + +Clarinet.test({ + name: "Ensure that <...>", + async fn(chain: Chain, accounts: Map) { + let block = chain.mineBlock([ + /* + * Add transactions with: + * Tx.contractCall(...) + */ + ]); + assertEquals(block.receipts.length, 0); + assertEquals(block.height, 2); + + block = chain.mineBlock([ + /* + * Add transactions with: + * Tx.contractCall(...) + */ + ]); + assertEquals(block.receipts.length, 0); + assertEquals(block.height, 3); + }, +}); From 86e48c942248004237ed7204be6acbe439045856 Mon Sep 17 00:00:00 2001 From: Amobi Ndubuisi Date: Wed, 18 Sep 2024 06:57:46 -0700 Subject: [PATCH 2/2] updated tokenlock smart contract --- securelock/contracts/tokensafe.clar | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/securelock/contracts/tokensafe.clar b/securelock/contracts/tokensafe.clar index 26c8e19..7275ff2 100644 --- a/securelock/contracts/tokensafe.clar +++ b/securelock/contracts/tokensafe.clar @@ -9,6 +9,8 @@ (define-constant ERR_INSUFFICIENT_BALANCE (err u104)) (define-constant ERR_INVALID_PARAMETER (err u105)) (define-constant ERR_TRANSFER_FAILED (err u106)) +(define-constant ERR_ALREADY_HAS_SCHEDULE (err u107)) +(define-constant ERR_INVALID_RECIPIENT (err u108)) ;; Define data variables (define-data-var token-name (string-ascii 32) "") @@ -59,6 +61,7 @@ (begin (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_UNAUTHORIZED) (asserts! (var-get contract-initialized) ERR_NOT_INITIALIZED) + (asserts! (is-none (map-get? vesting-schedules participant)) ERR_ALREADY_HAS_SCHEDULE) (asserts! (> total-allocation u0) ERR_INVALID_PARAMETER) (asserts! (>= start-block block-height) ERR_INVALID_PARAMETER) (asserts! (>= vesting-duration cliff-duration) ERR_INVALID_PARAMETER) @@ -130,11 +133,17 @@ (sender tx-sender) (sender-balance (unwrap! (get-balance sender) ERR_INVALID_PARAMETER)) ) + (asserts! (and (not (is-eq sender recipient)) (not (is-eq recipient (as-contract tx-sender)))) ERR_INVALID_RECIPIENT) (asserts! (>= sender-balance amount) ERR_INSUFFICIENT_BALANCE) - (map-set token-balances sender (- sender-balance amount)) - (map-set token-balances recipient - (+ (default-to u0 (map-get? token-balances recipient)) amount) - ) + (try! (as-contract (transfer-tokens sender recipient amount))) + (ok true) + ) +) + +(define-private (transfer-tokens (sender principal) (recipient principal) (amount uint)) + (begin + (map-set token-balances sender (- (unwrap! (get-balance sender) ERR_INVALID_PARAMETER) amount)) + (map-set token-balances recipient (+ (unwrap! (get-balance recipient) ERR_INVALID_PARAMETER) amount)) (ok true) ) )