diff --git a/crop-insurance/.gitignore b/crop-insurance/.gitignore new file mode 100644 index 0000000..f18b582 --- /dev/null +++ b/crop-insurance/.gitignore @@ -0,0 +1,4 @@ + +settings/Mainnet.toml +settings/Testnet.toml +history.txt diff --git a/crop-insurance/.vscode/settings.json b/crop-insurance/.vscode/settings.json new file mode 100644 index 0000000..02e21eb --- /dev/null +++ b/crop-insurance/.vscode/settings.json @@ -0,0 +1,4 @@ + +{ + "deno.enable": true, +} diff --git a/crop-insurance/.vscode/tasks.json b/crop-insurance/.vscode/tasks.json new file mode 100644 index 0000000..22af91c --- /dev/null +++ b/crop-insurance/.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/crop-insurance/Clarinet.toml b/crop-insurance/Clarinet.toml new file mode 100644 index 0000000..a76139f --- /dev/null +++ b/crop-insurance/Clarinet.toml @@ -0,0 +1,11 @@ +[project] +name = "crop-insurance" +authors = [] +description = "" +telemetry = true +requirements = [] +analysis = ["check_checker"] +costs_version = 2 +[contracts.automation] +path = "contracts/automation.clar" +depends_on = [] diff --git a/crop-insurance/contracts/automation.clar b/crop-insurance/contracts/automation.clar new file mode 100644 index 0000000..0ac5a4a --- /dev/null +++ b/crop-insurance/contracts/automation.clar @@ -0,0 +1,352 @@ +;; Crop Insurance Automation Smart Contract +;; Provides automated crop insurance with oracle-based claim processing + +;; Constants +(define-constant contract-owner tx-sender) +(define-constant err-owner-only (err u100)) +(define-constant err-not-found (err u101)) +(define-constant err-already-exists (err u102)) +(define-constant err-insufficient-funds (err u103)) +(define-constant err-invalid-amount (err u104)) +(define-constant err-policy-expired (err u105)) +(define-constant err-policy-not-active (err u106)) +(define-constant err-claim-exists (err u107)) +(define-constant err-unauthorized (err u108)) +(define-constant err-invalid-threshold (err u109)) +(define-constant err-invalid-input (err u110)) + +;; Data Variables +(define-data-var policy-counter uint u0) +(define-data-var claim-counter uint u0) +(define-data-var oracle-address principal contract-owner) +(define-data-var min-premium uint u1000000) ;; 1 STX in microSTX +(define-data-var max-coverage uint u100000000000) ;; 100,000 STX + +;; Data Maps +(define-map policies + { policy-id: uint } + { + farmer: principal, + crop-type: (string-ascii 50), + coverage-amount: uint, + premium-paid: uint, + start-block: uint, + end-block: uint, + location: (string-ascii 100), + rainfall-threshold: uint, + temperature-threshold: uint, + is-active: bool, + claim-filed: bool + } +) + +(define-map claims + { claim-id: uint } + { + policy-id: uint, + farmer: principal, + claim-amount: uint, + rainfall-actual: uint, + temperature-actual: uint, + filed-at: uint, + status: (string-ascii 20), + approved-by: (optional principal) + } +) + +(define-map farmer-policies + { farmer: principal } + { policy-ids: (list 50 uint) } +) + +(define-map authorized-oracles + { oracle: principal } + { is-authorized: bool } +) + +;; Read-only functions +(define-read-only (get-policy (policy-id uint)) + (map-get? policies { policy-id: policy-id }) +) + +(define-read-only (get-claim (claim-id uint)) + (map-get? claims { claim-id: claim-id }) +) + +(define-read-only (get-farmer-policies (farmer principal)) + (default-to { policy-ids: (list) } (map-get? farmer-policies { farmer: farmer })) +) + +(define-read-only (is-oracle-authorized (oracle principal)) + (default-to false (get is-authorized (map-get? authorized-oracles { oracle: oracle }))) +) + +(define-read-only (calculate-premium (coverage-amount uint) (risk-factor uint)) + (/ (* coverage-amount risk-factor) u10000) +) + +(define-read-only (get-contract-balance) + (stx-get-balance (as-contract tx-sender)) +) + +;; Private functions +(define-private (add-policy-to-farmer (farmer principal) (policy-id uint)) + (let ((current-policies (get policy-ids (get-farmer-policies farmer)))) + (map-set farmer-policies + { farmer: farmer } + { policy-ids: (unwrap-panic (as-max-len? (append current-policies policy-id) u50)) } + ) + ) +) + +;; Public functions - Policy Management +(define-public (create-policy + (crop-type (string-ascii 50)) + (coverage-amount uint) + (duration-blocks uint) + (location (string-ascii 100)) + (rainfall-threshold uint) + (temperature-threshold uint) + (risk-factor uint)) + (let + ( + (policy-id (+ (var-get policy-counter) u1)) + (premium (calculate-premium coverage-amount risk-factor)) + (start-block block-height) + ;; Validate duration-blocks before using + (validated-duration (begin + (asserts! (and (> duration-blocks u0) (<= duration-blocks u52560)) err-invalid-input) + duration-blocks)) + (end-block (+ block-height validated-duration)) + ;; Validate string inputs length + (validated-crop-type (begin + (asserts! (> (len crop-type) u0) err-invalid-input) + crop-type)) + (validated-location (begin + (asserts! (> (len location) u0) err-invalid-input) + location)) + ) + (asserts! (>= premium (var-get min-premium)) err-invalid-amount) + (asserts! (<= coverage-amount (var-get max-coverage)) err-invalid-amount) + (asserts! (> rainfall-threshold u0) err-invalid-threshold) + (asserts! (> temperature-threshold u0) err-invalid-threshold) + + (try! (stx-transfer? premium tx-sender (as-contract tx-sender))) + + (map-set policies + { policy-id: policy-id } + { + farmer: tx-sender, + crop-type: validated-crop-type, + coverage-amount: coverage-amount, + premium-paid: premium, + start-block: start-block, + end-block: end-block, + location: validated-location, + rainfall-threshold: rainfall-threshold, + temperature-threshold: temperature-threshold, + is-active: true, + claim-filed: false + } + ) + + (add-policy-to-farmer tx-sender policy-id) + (var-set policy-counter policy-id) + (ok policy-id) + ) +) + +(define-public (cancel-policy (policy-id uint)) + (let ((policy (unwrap! (get-policy policy-id) err-not-found))) + (asserts! (is-eq (get farmer policy) tx-sender) err-unauthorized) + (asserts! (get is-active policy) err-policy-not-active) + (asserts! (not (get claim-filed policy)) err-claim-exists) + (asserts! (< block-height (get end-block policy)) err-policy-expired) + + (map-set policies + { policy-id: policy-id } + (merge policy { is-active: false }) + ) + + ;; Refund 50% of premium for early cancellation + (let ((refund (/ (get premium-paid policy) u2))) + (try! (as-contract (stx-transfer? refund tx-sender (get farmer policy)))) + ) + (ok true) + ) +) + +;; Public functions - Claims Management +(define-public (file-claim + (policy-id uint) + (rainfall-actual uint) + (temperature-actual uint)) + (let + ( + (policy (unwrap! (get-policy policy-id) err-not-found)) + (claim-id (+ (var-get claim-counter) u1)) + ;; Validate weather data inputs before using + (validated-rainfall (begin + (asserts! (<= rainfall-actual u10000) err-invalid-input) + rainfall-actual)) + (validated-temperature (begin + (asserts! (<= temperature-actual u1000) err-invalid-input) + temperature-actual)) + ) + (asserts! (is-eq (get farmer policy) tx-sender) err-unauthorized) + (asserts! (get is-active policy) err-policy-not-active) + (asserts! (not (get claim-filed policy)) err-claim-exists) + (asserts! (>= block-height (get start-block policy)) err-policy-not-active) + (asserts! (<= block-height (get end-block policy)) err-policy-expired) + + (let ((claim-amount (calculate-claim-amount policy validated-rainfall validated-temperature))) + (map-set claims + { claim-id: claim-id } + { + policy-id: policy-id, + farmer: tx-sender, + claim-amount: claim-amount, + rainfall-actual: validated-rainfall, + temperature-actual: validated-temperature, + filed-at: block-height, + status: "pending", + approved-by: none + } + ) + + (map-set policies + { policy-id: policy-id } + (merge policy { claim-filed: true }) + ) + + (var-set claim-counter claim-id) + (ok claim-id) + ) + ) +) + +(define-private (calculate-claim-amount + (policy {farmer: principal, crop-type: (string-ascii 50), coverage-amount: uint, premium-paid: uint, + start-block: uint, end-block: uint, location: (string-ascii 100), + rainfall-threshold: uint, temperature-threshold: uint, is-active: bool, claim-filed: bool}) + (rainfall-actual uint) + (temperature-actual uint)) + (let + ( + (rainfall-deficit (if (< rainfall-actual (get rainfall-threshold policy)) + (- (get rainfall-threshold policy) rainfall-actual) + u0)) + (temp-excess (if (> temperature-actual (get temperature-threshold policy)) + (- temperature-actual (get temperature-threshold policy)) + u0)) + (total-impact (+ rainfall-deficit temp-excess)) + (coverage (get coverage-amount policy)) + (calculated-payout (if (> total-impact u0) + (/ (* coverage total-impact) (get rainfall-threshold policy)) + u0)) + ) + ;; Return the minimum of coverage or calculated payout + (if (<= calculated-payout coverage) + calculated-payout + coverage + ) + ) +) + +(define-public (approve-claim (claim-id uint)) + (let ((claim (unwrap! (get-claim claim-id) err-not-found))) + (asserts! (is-oracle-authorized tx-sender) err-unauthorized) + (asserts! (is-eq (get status claim) "pending") err-not-found) + + (map-set claims + { claim-id: claim-id } + (merge claim { + status: "approved", + approved-by: (some tx-sender) + }) + ) + + ;; Process payout + (if (> (get claim-amount claim) u0) + (try! (as-contract (stx-transfer? (get claim-amount claim) tx-sender (get farmer claim)))) + true + ) + (ok true) + ) +) + +(define-public (reject-claim (claim-id uint)) + (let ((claim (unwrap! (get-claim claim-id) err-not-found))) + (asserts! (is-oracle-authorized tx-sender) err-unauthorized) + (asserts! (is-eq (get status claim) "pending") err-not-found) + + (map-set claims + { claim-id: claim-id } + (merge claim { + status: "rejected", + approved-by: (some tx-sender) + }) + ) + (ok true) + ) +) + +;; Admin functions +(define-public (add-oracle (oracle principal)) + (begin + (asserts! (is-eq tx-sender contract-owner) err-owner-only) + ;; Validate oracle principal is not contract owner + (asserts! (not (is-eq oracle contract-owner)) err-invalid-input) + (map-set authorized-oracles + { oracle: oracle } + { is-authorized: true } + ) + (ok true) + ) +) + +(define-public (remove-oracle (oracle principal)) + (begin + (asserts! (is-eq tx-sender contract-owner) err-owner-only) + ;; Validate oracle exists and is authorized before removing + (asserts! (is-some (map-get? authorized-oracles { oracle: oracle })) err-not-found) + (asserts! (is-oracle-authorized oracle) err-not-found) + (map-delete authorized-oracles { oracle: oracle }) + (ok true) + ) +) + +(define-public (update-min-premium (new-min uint)) + (begin + (asserts! (is-eq tx-sender contract-owner) err-owner-only) + ;; Validate new minimum premium is reasonable + (asserts! (and (> new-min u0) (<= new-min u1000000000)) err-invalid-input) + (var-set min-premium new-min) + (ok true) + ) +) + +(define-public (update-max-coverage (new-max uint)) + (begin + (asserts! (is-eq tx-sender contract-owner) err-owner-only) + ;; Validate new maximum coverage is reasonable + (asserts! (and (> new-max u0) (>= new-max (var-get min-premium))) err-invalid-input) + (var-set max-coverage new-max) + (ok true) + ) +) + +(define-public (fund-contract (amount uint)) + (begin + (asserts! (is-eq tx-sender contract-owner) err-owner-only) + (stx-transfer? amount tx-sender (as-contract tx-sender)) + ) +) + +(define-public (withdraw-funds (amount uint) (recipient principal)) + (begin + (asserts! (is-eq tx-sender contract-owner) err-owner-only) + (asserts! (<= amount (get-contract-balance)) err-insufficient-funds) + (as-contract (stx-transfer? amount tx-sender recipient)) + ) +) diff --git a/crop-insurance/settings/Devnet.toml b/crop-insurance/settings/Devnet.toml new file mode 100644 index 0000000..8a5ff75 --- /dev/null +++ b/crop-insurance/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/crop-insurance/tests/automation_test.ts b/crop-insurance/tests/automation_test.ts new file mode 100644 index 0000000..9a18ae0 --- /dev/null +++ b/crop-insurance/tests/automation_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); + }, +});