diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock
index 27edc24d..d1e42608 100644
--- a/contracts/Cargo.lock
+++ b/contracts/Cargo.lock
@@ -23,7 +23,7 @@ version = "0.1.0"
dependencies = [
"contract-utils",
"harvesta-errors",
- "soroban-sdk",
+ "soroban-sdk 21.7.7",
]
[[package]]
@@ -31,7 +31,19 @@ name = "aggregate-impact-verifier"
version = "0.1.0"
dependencies = [
"harvesta-errors",
- "soroban-sdk",
+ "soroban-sdk 21.7.7",
+]
+
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "version_check",
+ "zerocopy",
]
[[package]]
@@ -52,6 +64,124 @@ dependencies = [
"derive_arbitrary",
]
+[[package]]
+name = "ark-bls12-381"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c775f0d12169cba7aae4caeb547bb6a50781c7449a8aa53793827c9ec4abf488"
+dependencies = [
+ "ark-ec",
+ "ark-ff",
+ "ark-serialize",
+ "ark-std",
+]
+
+[[package]]
+name = "ark-ec"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "defd9a439d56ac24968cca0571f598a61bc8c55f71d50a89cda591cb750670ba"
+dependencies = [
+ "ark-ff",
+ "ark-poly",
+ "ark-serialize",
+ "ark-std",
+ "derivative",
+ "hashbrown 0.13.2",
+ "itertools 0.10.5",
+ "num-traits",
+ "zeroize",
+]
+
+[[package]]
+name = "ark-ff"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba"
+dependencies = [
+ "ark-ff-asm",
+ "ark-ff-macros",
+ "ark-serialize",
+ "ark-std",
+ "derivative",
+ "digest",
+ "itertools 0.10.5",
+ "num-bigint",
+ "num-traits",
+ "paste",
+ "rustc_version",
+ "zeroize",
+]
+
+[[package]]
+name = "ark-ff-asm"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348"
+dependencies = [
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "ark-ff-macros"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565"
+dependencies = [
+ "num-bigint",
+ "num-traits",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "ark-poly"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d320bfc44ee185d899ccbadfa8bc31aab923ce1558716e1997a1e74057fe86bf"
+dependencies = [
+ "ark-ff",
+ "ark-serialize",
+ "ark-std",
+ "derivative",
+ "hashbrown 0.13.2",
+]
+
+[[package]]
+name = "ark-serialize"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5"
+dependencies = [
+ "ark-serialize-derive",
+ "ark-std",
+ "digest",
+ "num-bigint",
+]
+
+[[package]]
+name = "ark-serialize-derive"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae3281bc6d0fd7e549af32b52511e1302185bd688fd3359fa36423346ff682ea"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "ark-std"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185"
+dependencies = [
+ "num-traits",
+ "rand 0.8.6",
+]
+
[[package]]
name = "autocfg"
version = "1.5.0"
@@ -148,7 +278,7 @@ dependencies = [
"num-bigint",
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -189,7 +319,7 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
name = "contract-utils"
version = "0.1.0"
dependencies = [
- "soroban-sdk",
+ "soroban-sdk 21.7.7",
]
[[package]]
@@ -247,7 +377,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
dependencies = [
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -274,7 +404,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -308,7 +438,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -321,7 +451,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -332,7 +462,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core 0.20.11",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -343,9 +473,15 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
dependencies = [
"darling_core 0.23.0",
"quote",
- "syn",
+ "syn 2.0.117",
]
+[[package]]
+name = "data-encoding"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
+
[[package]]
name = "der"
version = "0.7.10"
@@ -366,6 +502,17 @@ dependencies = [
"serde_core",
]
+[[package]]
+name = "derivative"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
[[package]]
name = "derive_arbitrary"
version = "1.3.2"
@@ -374,7 +521,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -395,7 +542,7 @@ version = "0.1.0"
dependencies = [
"contract-utils",
"harvesta-errors",
- "soroban-sdk",
+ "soroban-sdk 21.7.7",
]
[[package]]
@@ -498,7 +645,7 @@ checksum = "2bfcf67fea2815c2fc3b90873fae90957be12ff417335dfadc7f52927feb03b2"
name = "escrow"
version = "0.1.0"
dependencies = [
- "soroban-sdk",
+ "soroban-sdk 21.7.7",
]
[[package]]
@@ -507,7 +654,7 @@ version = "0.1.0"
dependencies = [
"contract-utils",
"harvesta-errors",
- "soroban-sdk",
+ "soroban-sdk 21.7.7",
]
[[package]]
@@ -521,7 +668,7 @@ name = "farmer-registry"
version = "0.1.0"
dependencies = [
"harvesta-errors",
- "soroban-sdk",
+ "soroban-sdk 21.7.7",
]
[[package]]
@@ -626,7 +773,7 @@ dependencies = [
name = "harvesta-errors"
version = "0.1.0"
dependencies = [
- "soroban-sdk",
+ "soroban-sdk 21.7.7",
]
[[package]]
@@ -635,6 +782,15 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+[[package]]
+name = "hashbrown"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
+dependencies = [
+ "ahash",
+]
+
[[package]]
name = "hashbrown"
version = "0.17.0"
@@ -724,6 +880,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e04e2fd2b8188ea827b32ef11de88377086d690286ab35747ef7f9bf3ccb590"
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
[[package]]
name = "itertools"
version = "0.11.0"
@@ -775,7 +940,7 @@ name = "kyc-attestation"
version = "0.1.0"
dependencies = [
"harvesta-errors",
- "soroban-sdk",
+ "soroban-sdk 21.7.7",
]
[[package]]
@@ -801,7 +966,7 @@ name = "location-proof"
version = "0.1.0"
dependencies = [
"harvesta-errors",
- "soroban-sdk",
+ "soroban-sdk 21.7.7",
]
[[package]]
@@ -831,7 +996,15 @@ version = "0.1.0"
dependencies = [
"contract-utils",
"harvesta-errors",
- "soroban-sdk",
+ "soroban-sdk 21.7.7",
+]
+
+[[package]]
+name = "nft-certificate"
+version = "0.1.0"
+dependencies = [
+ "harvesta-errors",
+ "soroban-sdk 21.7.7",
]
[[package]]
@@ -839,7 +1012,7 @@ name = "nullifier-registry"
version = "0.1.0"
dependencies = [
"harvesta-errors",
- "soroban-sdk",
+ "soroban-sdk 21.7.7",
]
[[package]]
@@ -866,7 +1039,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -930,6 +1103,20 @@ dependencies = [
"spki",
]
+[[package]]
+name = "planter-registry"
+version = "0.1.0"
+dependencies = [
+ "soroban-sdk 21.7.7",
+]
+
+[[package]]
+name = "platform-governance"
+version = "0.1.0"
+dependencies = [
+ "soroban-sdk 21.7.7",
+]
+
[[package]]
name = "powerfmt"
version = "0.2.0"
@@ -952,7 +1139,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -1104,7 +1291,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -1239,7 +1426,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -1283,7 +1470,7 @@ dependencies = [
"darling 0.23.0",
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -1335,10 +1522,22 @@ version = "21.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f57a68ef8777e28e274de0f3a88ad9a5a41d9a2eb461b4dd800b086f0e83b80"
dependencies = [
- "itertools",
+ "itertools 0.11.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "soroban-builtin-sdk-macros"
+version = "22.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf2e42bf80fcdefb3aae6ff3c7101a62cf942e95320ed5b518a1705bc11c6b2f"
+dependencies = [
+ "itertools 0.10.5",
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -1353,10 +1552,29 @@ dependencies = [
"num-derive",
"num-traits",
"serde",
- "soroban-env-macros",
+ "soroban-env-macros 21.2.1",
"soroban-wasmi",
"static_assertions",
- "stellar-xdr",
+ "stellar-xdr 21.2.0",
+ "wasmparser",
+]
+
+[[package]]
+name = "soroban-env-common"
+version = "22.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "027cd856171bfd6ad2c0ffb3b7dfe55ad7080fb3050c36ad20970f80da634472"
+dependencies = [
+ "arbitrary",
+ "crate-git-revision",
+ "ethnum",
+ "num-derive",
+ "num-traits",
+ "serde",
+ "soroban-env-macros 22.1.3",
+ "soroban-wasmi",
+ "static_assertions",
+ "stellar-xdr 22.1.0",
"wasmparser",
]
@@ -1366,7 +1584,17 @@ version = "21.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bfb2536811045d5cd0c656a324cbe9ce4467eb734c7946b74410d90dea5d0ce"
dependencies = [
- "soroban-env-common",
+ "soroban-env-common 21.2.1",
+ "static_assertions",
+]
+
+[[package]]
+name = "soroban-env-guest"
+version = "22.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a07dda1ae5220d975979b19ad4fd56bc86ec7ec1b4b25bc1c5d403f934e592e"
+dependencies = [
+ "soroban-env-common 22.1.3",
"static_assertions",
]
@@ -1395,11 +1623,47 @@ dependencies = [
"sec1",
"sha2",
"sha3",
- "soroban-builtin-sdk-macros",
- "soroban-env-common",
+ "soroban-builtin-sdk-macros 21.2.1",
+ "soroban-env-common 21.2.1",
"soroban-wasmi",
"static_assertions",
- "stellar-strkey",
+ "stellar-strkey 0.0.8",
+ "wasmparser",
+]
+
+[[package]]
+name = "soroban-env-host"
+version = "22.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66e8b03a4191d485eab03f066336112b2a50541a7553179553dc838b986b94dd"
+dependencies = [
+ "ark-bls12-381",
+ "ark-ec",
+ "ark-ff",
+ "ark-serialize",
+ "curve25519-dalek",
+ "ecdsa",
+ "ed25519-dalek",
+ "elliptic-curve",
+ "generic-array",
+ "getrandom 0.2.17",
+ "hex-literal",
+ "hmac",
+ "k256",
+ "num-derive",
+ "num-integer",
+ "num-traits",
+ "p256",
+ "rand 0.8.6",
+ "rand_chacha 0.3.1",
+ "sec1",
+ "sha2",
+ "sha3",
+ "soroban-builtin-sdk-macros 22.1.3",
+ "soroban-env-common 22.1.3",
+ "soroban-wasmi",
+ "static_assertions",
+ "stellar-strkey 0.0.9",
"wasmparser",
]
@@ -1409,13 +1673,28 @@ version = "21.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "242926fe5e0d922f12d3796cd7cd02dd824e5ef1caa088f45fce20b618309f64"
dependencies = [
- "itertools",
+ "itertools 0.11.0",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_json",
+ "stellar-xdr 21.2.0",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "soroban-env-macros"
+version = "22.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00eff744764ade3bc480e4909e3a581a240091f3d262acdce80b41f7069b2bd9"
+dependencies = [
+ "itertools 0.10.5",
"proc-macro2",
"quote",
"serde",
"serde_json",
- "stellar-xdr",
- "syn",
+ "stellar-xdr 22.1.0",
+ "syn 2.0.117",
]
[[package]]
@@ -1427,8 +1706,22 @@ dependencies = [
"serde",
"serde_json",
"serde_with",
- "soroban-env-common",
- "soroban-env-host",
+ "soroban-env-common 21.2.1",
+ "soroban-env-host 21.2.1",
+ "thiserror",
+]
+
+[[package]]
+name = "soroban-ledger-snapshot"
+version = "22.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c30035cf1e8f02f65de3e594b6da113ecdaf1cd134d8480961d62568bb15adaf"
+dependencies = [
+ "serde",
+ "serde_json",
+ "serde_with",
+ "soroban-env-common 22.1.3",
+ "soroban-env-host 22.1.3",
"thiserror",
]
@@ -1447,11 +1740,33 @@ dependencies = [
"rustc_version",
"serde",
"serde_json",
- "soroban-env-guest",
- "soroban-env-host",
- "soroban-ledger-snapshot",
- "soroban-sdk-macros",
- "stellar-strkey",
+ "soroban-env-guest 21.2.1",
+ "soroban-env-host 21.2.1",
+ "soroban-ledger-snapshot 21.7.7",
+ "soroban-sdk-macros 21.7.7",
+ "stellar-strkey 0.0.8",
+]
+
+[[package]]
+name = "soroban-sdk"
+version = "22.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff18e8d7ca6d5340a211605ca2c86383bd4dfacc4f8253d72a1573974ffffe69"
+dependencies = [
+ "arbitrary",
+ "bytes-lit",
+ "ctor",
+ "derive_arbitrary",
+ "ed25519-dalek",
+ "rand 0.8.6",
+ "rustc_version",
+ "serde",
+ "serde_json",
+ "soroban-env-guest 22.1.3",
+ "soroban-env-host 22.1.3",
+ "soroban-ledger-snapshot 22.0.11",
+ "soroban-sdk-macros 22.0.11",
+ "stellar-strkey 0.0.9",
]
[[package]]
@@ -1462,16 +1777,36 @@ checksum = "0974e413731aeff2443f2305b344578b3f1ffd18335a7ba0f0b5d2eb4e94c9ce"
dependencies = [
"crate-git-revision",
"darling 0.20.11",
- "itertools",
+ "itertools 0.11.0",
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "sha2",
+ "soroban-env-common 21.2.1",
+ "soroban-spec 21.7.7",
+ "soroban-spec-rust 21.7.7",
+ "stellar-xdr 21.2.0",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "soroban-sdk-macros"
+version = "22.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42b205cd86b34d530db87667bd287fbb194166d79b368227fd842110a914fde8"
+dependencies = [
+ "crate-git-revision",
+ "darling 0.20.11",
+ "itertools 0.10.5",
"proc-macro2",
"quote",
"rustc_version",
"sha2",
- "soroban-env-common",
- "soroban-spec",
- "soroban-spec-rust",
- "stellar-xdr",
- "syn",
+ "soroban-env-common 22.1.3",
+ "soroban-spec 22.0.11",
+ "soroban-spec-rust 22.0.11",
+ "stellar-xdr 22.1.0",
+ "syn 2.0.117",
]
[[package]]
@@ -1481,7 +1816,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2c70b20e68cae3ef700b8fa3ae29db1c6a294b311fba66918f90cb8f9fd0a1a"
dependencies = [
"base64 0.13.1",
- "stellar-xdr",
+ "stellar-xdr 21.2.0",
+ "thiserror",
+ "wasmparser",
+]
+
+[[package]]
+name = "soroban-spec"
+version = "22.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb6a16f2de28852c759f4da5f28cda54ec0d8dfa4c0e6e8cb3495234a72b0cea"
+dependencies = [
+ "base64 0.13.1",
+ "stellar-xdr 22.1.0",
"thiserror",
"wasmparser",
]
@@ -1496,9 +1843,25 @@ dependencies = [
"proc-macro2",
"quote",
"sha2",
- "soroban-spec",
- "stellar-xdr",
- "syn",
+ "soroban-spec 21.7.7",
+ "stellar-xdr 21.2.0",
+ "syn 2.0.117",
+ "thiserror",
+]
+
+[[package]]
+name = "soroban-spec-rust"
+version = "22.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdc6db5902ab21290dddf63fec4ee95703fe59891a947646e7b8607536f043fc"
+dependencies = [
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "sha2",
+ "soroban-spec 22.0.11",
+ "stellar-xdr 22.1.0",
+ "syn 2.0.117",
"thiserror",
]
@@ -1520,7 +1883,14 @@ name = "species-registry"
version = "0.1.0"
dependencies = [
"harvesta-errors",
- "soroban-sdk",
+ "soroban-sdk 21.7.7",
+]
+
+[[package]]
+name = "species-voting"
+version = "0.1.0"
+dependencies = [
+ "soroban-sdk 21.7.7",
]
[[package]]
@@ -1556,6 +1926,17 @@ dependencies = [
"thiserror",
]
+[[package]]
+name = "stellar-strkey"
+version = "0.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e3aa3ed00e70082cb43febc1c2afa5056b9bb3e348bbb43d0cd0aa88a611144"
+dependencies = [
+ "crate-git-revision",
+ "data-encoding",
+ "thiserror",
+]
+
[[package]]
name = "stellar-xdr"
version = "21.2.0"
@@ -1569,7 +1950,23 @@ dependencies = [
"hex",
"serde",
"serde_with",
- "stellar-strkey",
+ "stellar-strkey 0.0.8",
+]
+
+[[package]]
+name = "stellar-xdr"
+version = "22.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ce69db907e64d1e70a3dce8d4824655d154749426a6132b25395c49136013e4"
+dependencies = [
+ "arbitrary",
+ "base64 0.13.1",
+ "crate-git-revision",
+ "escape-bytes",
+ "hex",
+ "serde",
+ "serde_with",
+ "stellar-strkey 0.0.9",
]
[[package]]
@@ -1582,7 +1979,7 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
name = "subscription-sponsorship"
version = "0.1.0"
dependencies = [
- "soroban-sdk",
+ "soroban-sdk 21.7.7",
]
[[package]]
@@ -1591,6 +1988,17 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
[[package]]
name = "syn"
version = "2.0.117"
@@ -1632,7 +2040,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -1666,6 +2074,13 @@ dependencies = [
"time-core",
]
+[[package]]
+name = "treasury"
+version = "0.1.0"
+dependencies = [
+ "soroban-sdk 22.0.11",
+]
+
[[package]]
name = "tree-escrow"
version = "0.1.0"
@@ -1673,7 +2088,7 @@ dependencies = [
"contract-utils",
"harvesta-errors",
"proptest",
- "soroban-sdk",
+ "soroban-sdk 21.7.7",
]
[[package]]
@@ -1682,7 +2097,7 @@ version = "0.1.0"
dependencies = [
"contract-utils",
"harvesta-errors",
- "soroban-sdk",
+ "soroban-sdk 21.7.7",
]
[[package]]
@@ -1765,7 +2180,7 @@ dependencies = [
"bumpalo",
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
"wasm-bindgen-shared",
]
@@ -1836,7 +2251,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -1847,7 +2262,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -1906,7 +2321,7 @@ checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -1914,13 +2329,27 @@ name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+dependencies = [
+ "zeroize_derive",
+]
+
+[[package]]
+name = "zeroize_derive"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
[[package]]
name = "zk-location-verifier"
version = "0.1.0"
dependencies = [
"harvesta-errors",
- "soroban-sdk",
+ "soroban-sdk 21.7.7",
]
[[package]]
diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml
index f51139ae..6df246c8 100644
--- a/contracts/Cargo.toml
+++ b/contracts/Cargo.toml
@@ -18,6 +18,7 @@ members = [
"species-voting",
"treasury",
"platform-governance",
+ "nft-certificate",
"subscription-sponsorship",
"planter-registry",
]
diff --git a/contracts/harvesta-errors/src/lib.rs b/contracts/harvesta-errors/src/lib.rs
index 1e1f18b0..4a45426d 100644
--- a/contracts/harvesta-errors/src/lib.rs
+++ b/contracts/harvesta-errors/src/lib.rs
@@ -5,13 +5,14 @@
//! Import the crate, then call `panic_with_error!(env, HarvestaError::Variant)`
//! instead of raw string panics. Error codes are stable u32 values embedded in
//! the Stellar XDR so off-chain tooling can parse them without string matching.
-//!
-//! NOTE: Error count reduced to stay within Soroban SDK limits.
-//! Only essential errors for current contracts are included.
use soroban_sdk::contracterror;
-#[contracterror]
+// Export is disabled because the consolidated error enum exceeds the Soroban
+// XDR spec limit of 50 cases for `contracterror` types. The enum still
+// implements all traits required by `panic_with_error!`, so contracts can
+// continue to use it; it simply won't be embedded in the contract spec XDR.
+#[contracterror(export = false)]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum HarvestaError {
@@ -35,50 +36,115 @@ pub enum HarvestaError {
SlotAmountMustBePositive = 15,
// ── Escrow state (16–25) ──────────────────────────────────────────────────
- // EscrowAlreadyExists = 16,
- // EscrowNotFound = 17,
- // PlantingAlreadyVerified = 18,
- // PlantingNotVerified = 19,
- // RefundAfterPlanting = 20,
- // SurvivalThresholdOutOfRange = 21,
- // SurvivalRateOutOfRange = 22,
- // SurvivalRateBelowMinimum = 23,
- // SurvivalPeriodNotElapsed = 24,
- // NothingToRelease = 25,
+ EscrowAlreadyExists = 16,
+ EscrowNotFound = 17,
+ PlantingAlreadyVerified = 18,
+ PlantingNotVerified = 19,
+ RefundAfterPlanting = 20,
+ SurvivalThresholdOutOfRange = 21,
+ SurvivalRateOutOfRange = 22,
+ SurvivalRateBelowMinimum = 23,
+ SurvivalPeriodNotElapsed = 24,
+ NothingToRelease = 25,
// ── Oracle / tree co-fund (26–34) ─────────────────────────────────────────
- // UnauthorizedOracle = 26,
- // NoOracleReport = 27,
- // BatchEmpty = 28,
- // BatchTooLarge = 29,
- // TreeAlreadyRegistered = 30,
- // TreeNotRegistered = 31,
- // TreeNotOpenForContributions = 32,
- // TreeNotOpenForRelease = 33,
- // NoFundsToRelease = 34,
+ UnauthorizedOracle = 26,
+ NoOracleReport = 27,
+ BatchEmpty = 28,
+ BatchTooLarge = 29,
+ TreeAlreadyRegistered = 30,
+ TreeNotRegistered = 31,
+ TreeNotOpenForContributions = 32,
+ TreeNotOpenForRelease = 33,
+ NoFundsToRelease = 34,
// ── Farmer registry (35–37) ───────────────────────────────────────────────
- // FarmerAlreadyRegistered = 35,
- // FarmerNotRegistered = 36,
- // InvalidRegion = 37,
+ FarmerAlreadyRegistered = 35,
+ FarmerNotRegistered = 36,
+ InvalidRegion = 37,
// ── Dispute / arbiter (38–46) ─────────────────────────────────────────────
- // DisputeAlreadyOpen = 38,
- // NoOpenDispute = 39,
- // EscrowAlreadyFinalised = 40,
- // NotArbiter = 41,
- // NotBuyerOrSeller = 42,
- // MilestoneReleaseBlocked = 43,
- // MilestoneAlreadyProcessed = 44,
- // CompletionPercentageOutOfRange = 45,
- // TotalReleasedExceedsMilestone = 46,
+ DisputeAlreadyOpen = 38,
+ NoOpenDispute = 39,
+ EscrowAlreadyFinalised = 40,
+ NotArbiter = 41,
+ NotBuyerOrSeller = 42,
+ MilestoneReleaseBlocked = 43,
+ MilestoneAlreadyProcessed = 44,
+ CompletionPercentageOutOfRange = 45,
+ TotalReleasedExceedsMilestone = 46,
+
+ // ── Naira payout (47–54) ──────────────────────────────────────────────────
+ PendingPayoutAlreadyExists = 47,
+ PayoutIntervalTooShort = 48,
+ MaxDailyPayoutExceeded = 49,
+ PayoutNotPending = 50,
+ CanOnlyCancelPending = 51,
+ PayoutNotFound = 52,
+ ExpectedNgnMustBePositive = 53,
+ UnsupportedToken = 54,
+
+ // ── Aggregate impact verifier (55–59) ─────────────────────────────────────
+ FarmCountMustBePositive = 55,
+ PeriodEndBeforeStart = 56,
+ ProofDigestAlreadyRegistered = 57,
+ ProofNotFound = 58,
+ ProofAlreadyRevoked = 59,
+
+ // ── Nullifier registry (60) ───────────────────────────────────────────────
+ CommitmentAlreadyRegistered = 60,
+
+ // ── KYC attestation (61) ──────────────────────────────────────────────────
+ NotVerifier = 61,
// ── Species registry (62–64) ──────────────────────────────────────────────
Co2MustBePositive = 62,
MaturityYearsMustBePositive = 63,
SpeciesNotFound = 64,
+ // ── Location / ZK proofs (65–70) ──────────────────────────────────────────
+ OutsideNigeriaRegion = 65,
+ ProofCommitmentAlreadyRegistered = 66,
+ CommitmentAlreadySubmitted = 67,
+ CommitmentNotFound = 68,
+ CommitmentNotPending = 69,
+ InvalidProof = 70,
+
+ // ── Donation escrow (71–79) ───────────────────────────────────────────────
+ AlreadyProcessed = 71,
+ NotDonor = 72,
+ DonationAlreadyCancelled = 73,
+ DonationCancelled = 74,
+ IntervalNotElapsed = 75,
+ ProjectNotRegistered = 76,
+ AmountPerIntervalMustBePositive = 77,
+ IntervalSecondsMustBePositive = 78,
+ RecurringDonationNotFound = 79,
+
// ── Arithmetic overflows (80–81) ──────────────────────────────────────────
TreeTokenMintOverflow = 80,
TokenUnitOverflow = 81,
+
+ // ── NFT certificate (90–93) ───────────────────────────────────────────────
+ TokenAlreadyMinted = 90,
+ InvalidPlantingDate = 91,
+ TokenNotFound = 92,
+ NotTokenOwner = 93,
+
+ // ── Verifier staking (94–97) ──────────────────────────────────────────────
+ // NOTE: renumbered from the original 91–95 range to avoid collision with
+ // the NFT certificate errors introduced in the same feature branch.
+ MinStakeMustBePositive = 94,
+ VerifierNotStaked = 95,
+ SlashExceedsStake = 96,
+ InsufficientStake = 97,
+
+ // ── Carbon credit marketplace (101–107) ───────────────────────────────────
+ ListingAmountMustBePositive = 101,
+ PriceMustBePositive = 102,
+ ListingNotFound = 103,
+ ListingNotActive = 104,
+ BuyAmountMustBePositive = 105,
+ InsufficientLiquidity = 106,
+ SelfTrade = 107,
}
diff --git a/contracts/nft-certificate/Cargo.toml b/contracts/nft-certificate/Cargo.toml
new file mode 100644
index 00000000..80ef0b1a
--- /dev/null
+++ b/contracts/nft-certificate/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "nft-certificate"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[dependencies]
+soroban-sdk = { version = "21.7.7", features = ["alloc"] }
+harvesta-errors = { path = "../harvesta-errors" }
+
+[dev-dependencies]
+soroban-sdk = { version = "21.7.7", features = ["testutils", "alloc"] }
+
+[profile.release]
+opt-level = "z"
+overflow-checks = true
+debug = 0
+strip = "symbols"
+debug-assertions = false
+panic = "abort"
+codegen-units = 1
+lto = true
diff --git a/contracts/nft-certificate/src/lib.rs b/contracts/nft-certificate/src/lib.rs
new file mode 100644
index 00000000..f986a45c
--- /dev/null
+++ b/contracts/nft-certificate/src/lib.rs
@@ -0,0 +1,373 @@
+#![no_std]
+
+//! NFT Certificate Contract
+//!
+//! SEP-41 style non-fungible token for donor impact certificates.
+//! Each certificate records immutable on-chain metadata: tree count, CO2 offset,
+//! planting date, and region. The contract enforces positive impact values and
+//! rejects planting dates set in the future.
+//!
+//! # Storage layout
+//! Instance:
+//! ADMIN — Address (contract admin)
+//! NAME — String (token collection name)
+//! SYMBOL — String (token collection symbol)
+//! Persistent (keyed by token_id):
+//! TokenData — owner, approved operator, metadata URI
+
+use harvesta_errors::HarvestaError;
+use soroban_sdk::{
+ contract, contractimpl, contracttype, panic_with_error, Address, Env, String, Symbol,
+};
+
+// ── Types ─────────────────────────────────────────────────────────────────────
+
+/// On-chain data stored for each certificate NFT.
+#[contracttype]
+#[derive(Clone, Debug)]
+pub struct TokenData {
+ /// Current owner of the certificate
+ pub owner: Address,
+ /// Approved operator, if any
+ pub approved: Option
,
+ /// URI pointing to the off-chain JSON metadata
+ pub uri: String,
+}
+
+/// Storage keys.
+#[contracttype]
+#[derive(Clone, Debug)]
+pub enum DataKey {
+ /// Contract admin address
+ Admin,
+ /// Collection name
+ Name,
+ /// Collection symbol
+ Symbol,
+ /// Token data by token id
+ Token(String),
+}
+
+// ── Contract ──────────────────────────────────────────────────────────────────
+
+#[contract]
+pub struct NftCertificate;
+
+#[contractimpl]
+impl NftCertificate {
+ /// One-time initialisation.
+ ///
+ /// `admin` — address authorized to mint certificates
+ /// `name` — collection name
+ /// `symbol` — collection symbol
+ pub fn initialize(env: Env, admin: Address, name: String, symbol: String) {
+ if env.storage().instance().has(&DataKey::Admin) {
+ panic_with_error!(&env, HarvestaError::AlreadyInitialized);
+ }
+ env.storage().instance().set(&DataKey::Admin, &admin);
+ env.storage().instance().set(&DataKey::Name, &name);
+ env.storage().instance().set(&DataKey::Symbol, &symbol);
+
+ env.events().publish(
+ (Symbol::new(&env, "Initialize"), admin),
+ (name, symbol),
+ );
+ }
+
+ /// Mint a new certificate NFT to `recipient`.
+ ///
+ /// Validates impact metadata on-chain before minting:
+ /// - `tree_count` must be > 0
+ /// - `co2_offset_kg` must be > 0
+ /// - `planting_date` must not be in the future
+ ///
+ /// `token_id` must be unique; duplicates are rejected with `TokenAlreadyMinted`.
+ pub fn mint(
+ env: Env,
+ recipient: Address,
+ token_id: String,
+ metadata_uri: String,
+ tree_count: u32,
+ co2_offset_kg: u32,
+ planting_date: u64,
+ ) {
+ Self::require_admin(&env);
+
+ if env.storage().persistent().has(&DataKey::Token(token_id.clone())) {
+ panic_with_error!(&env, HarvestaError::TokenAlreadyMinted);
+ }
+
+ // On-chain validation of certificate metadata.
+ if tree_count == 0 {
+ panic_with_error!(&env, HarvestaError::TreeCountMustBePositive);
+ }
+ if co2_offset_kg == 0 {
+ panic_with_error!(&env, HarvestaError::Co2MustBePositive);
+ }
+ let now = env.ledger().timestamp();
+ if planting_date > now {
+ panic_with_error!(&env, HarvestaError::InvalidPlantingDate);
+ }
+
+ let token_data = TokenData {
+ owner: recipient.clone(),
+ approved: None,
+ uri: metadata_uri.clone(),
+ };
+ env.storage()
+ .persistent()
+ .set(&DataKey::Token(token_id.clone()), &token_data);
+
+ env.events().publish(
+ (Symbol::new(&env, "CertificateMinted"), recipient),
+ (token_id, metadata_uri, tree_count, co2_offset_kg, planting_date),
+ );
+ }
+
+ /// Returns the owner of `token_id`, if it exists.
+ pub fn owner_of(env: Env, token_id: String) -> Option {
+ env.storage()
+ .persistent()
+ .get(&DataKey::Token(token_id))
+ .map(|t: TokenData| t.owner)
+ }
+
+ /// Returns the metadata URI for `token_id`, if it exists.
+ pub fn token_uri(env: Env, token_id: String) -> Option {
+ env.storage()
+ .persistent()
+ .get(&DataKey::Token(token_id))
+ .map(|t: TokenData| t.uri)
+ }
+
+ /// Returns the approved operator for `token_id`, if any.
+ pub fn get_approved(env: Env, token_id: String) -> Option {
+ env.storage()
+ .persistent()
+ .get(&DataKey::Token(token_id))
+ .and_then(|t: TokenData| t.approved)
+ }
+
+ /// Returns the collection name.
+ pub fn name(env: Env) -> String {
+ env.storage()
+ .instance()
+ .get(&DataKey::Name)
+ .expect("not initialized")
+ }
+
+ /// Returns the collection symbol.
+ pub fn symbol(env: Env) -> String {
+ env.storage()
+ .instance()
+ .get(&DataKey::Symbol)
+ .expect("not initialized")
+ }
+
+ /// Approve `operator` to manage `token_id`. Owner only.
+ pub fn approve(env: Env, operator: Option, token_id: String) {
+ let mut token: TokenData = env
+ .storage()
+ .persistent()
+ .get(&DataKey::Token(token_id.clone()))
+ .unwrap_or_else(|| panic_with_error!(&env, HarvestaError::TokenNotFound));
+
+ token.owner.require_auth();
+ token.approved = operator.clone();
+ env.storage()
+ .persistent()
+ .set(&DataKey::Token(token_id.clone()), &token);
+
+ env.events().publish(
+ (Symbol::new(&env, "Approval"), token.owner, operator),
+ token_id,
+ );
+ }
+
+ /// Transfer `token_id` from `from` to `to`. Owner only.
+ pub fn transfer(env: Env, to: Address, token_id: String) {
+ let mut token: TokenData = env
+ .storage()
+ .persistent()
+ .get(&DataKey::Token(token_id.clone()))
+ .unwrap_or_else(|| panic_with_error!(&env, HarvestaError::TokenNotFound));
+
+ token.owner.require_auth();
+ let from = token.owner.clone();
+ token.owner = to.clone();
+ token.approved = None;
+ env.storage()
+ .persistent()
+ .set(&DataKey::Token(token_id.clone()), &token);
+
+ env.events().publish(
+ (Symbol::new(&env, "Transfer"), from, to),
+ token_id,
+ );
+ }
+
+ /// Transfer `token_id` from `from` to `to` by an approved operator.
+ pub fn transfer_from(env: Env, spender: Address, from: Address, to: Address, token_id: String) {
+ let mut token: TokenData = env
+ .storage()
+ .persistent()
+ .get(&DataKey::Token(token_id.clone()))
+ .unwrap_or_else(|| panic_with_error!(&env, HarvestaError::TokenNotFound));
+
+ if token.owner != from {
+ panic_with_error!(&env, HarvestaError::NotTokenOwner);
+ }
+
+ spender.require_auth();
+ let is_approved = token
+ .approved
+ .as_ref()
+ .map(|a| a == &spender)
+ .unwrap_or(false);
+ if token.owner != spender && !is_approved {
+ panic_with_error!(&env, HarvestaError::NotTokenOwner);
+ }
+
+ token.owner = to.clone();
+ token.approved = None;
+ env.storage()
+ .persistent()
+ .set(&DataKey::Token(token_id.clone()), &token);
+
+ env.events().publish(
+ (Symbol::new(&env, "Transfer"), from, to),
+ token_id,
+ );
+ }
+
+ // ── Internal ────────────────────────────────────────────────────────────────
+
+ fn require_admin(env: &Env) {
+ let admin: Address = env
+ .storage()
+ .instance()
+ .get(&DataKey::Admin)
+ .unwrap_or_else(|| panic_with_error!(env, HarvestaError::NotInitialized));
+ admin.require_auth();
+ }
+}
+
+// ── Tests ─────────────────────────────────────────────────────────────────────
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use soroban_sdk::{testutils::Address as _, Address, Env, String};
+
+ fn setup() -> (Env, Address, Address, NftCertificateClient<'static>) {
+ let env = Env::default();
+ env.mock_all_auths();
+
+ let contract_id = env.register_contract(None, NftCertificate);
+ let client = NftCertificateClient::new(&env, &contract_id);
+
+ let admin = Address::generate(&env);
+ let recipient = Address::generate(&env);
+
+ client.initialize(
+ &admin,
+ &String::from_str(&env, "Harvesta Impact Certificate"),
+ &String::from_str(&env, "HIC"),
+ );
+
+ (env, admin, recipient, client)
+ }
+
+ fn token_id(env: &Env) -> String {
+ String::from_str(env, "abc123")
+ }
+
+ fn uri(env: &Env) -> String {
+ String::from_str(env, "https://example.com/metadata/abc123.json")
+ }
+
+ #[test]
+ fn test_initialize() {
+ let (env, _, _, client) = setup();
+ assert_eq!(client.name(), String::from_str(&env, "Harvesta Impact Certificate"));
+ assert_eq!(client.symbol(), String::from_str(&env, "HIC"));
+ }
+
+ #[test]
+ fn test_mint_valid_certificate() {
+ let (env, _, recipient, client) = setup();
+
+ let id = token_id(&env);
+ let metadata = uri(&env);
+ let now = env.ledger().timestamp();
+
+ client.mint(&recipient, &id, &metadata, &100, &4800, &now);
+
+ assert_eq!(client.owner_of(&id), Some(recipient));
+ assert_eq!(client.token_uri(&id), Some(metadata));
+ }
+
+ #[test]
+ #[should_panic(expected = "Error(Contract, #90)")]
+ fn test_duplicate_token_rejected() {
+ let (env, _, recipient, client) = setup();
+
+ let id = token_id(&env);
+ let metadata = uri(&env);
+ let now = env.ledger().timestamp();
+
+ client.mint(&recipient, &id, &metadata, &100, &4800, &now);
+ client.mint(&recipient, &id, &metadata, &50, &2400, &now);
+ }
+
+ #[test]
+ #[should_panic(expected = "Error(Contract, #10)")]
+ fn test_zero_tree_count_rejected() {
+ let (env, _, recipient, client) = setup();
+
+ let id = token_id(&env);
+ let metadata = uri(&env);
+ let now = env.ledger().timestamp();
+
+ client.mint(&recipient, &id, &metadata, &0, &4800, &now);
+ }
+
+ #[test]
+ #[should_panic(expected = "Error(Contract, #62)")]
+ fn test_zero_co2_offset_rejected() {
+ let (env, _, recipient, client) = setup();
+
+ let id = token_id(&env);
+ let metadata = uri(&env);
+ let now = env.ledger().timestamp();
+
+ client.mint(&recipient, &id, &metadata, &100, &0, &now);
+ }
+
+ #[test]
+ #[should_panic(expected = "Error(Contract, #91)")]
+ fn test_future_planting_date_rejected() {
+ let (env, _, recipient, client) = setup();
+
+ let id = token_id(&env);
+ let metadata = uri(&env);
+ let now = env.ledger().timestamp();
+
+ client.mint(&recipient, &id, &metadata, &100, &4800, &(now + 1));
+ }
+
+ #[test]
+ fn test_transfer() {
+ let (env, _, recipient, client) = setup();
+ let new_owner = Address::generate(&env);
+
+ let id = token_id(&env);
+ let metadata = uri(&env);
+ let now = env.ledger().timestamp();
+
+ client.mint(&recipient, &id, &metadata, &100, &4800, &now);
+ client.transfer(&new_owner, &id);
+
+ assert_eq!(client.owner_of(&id), Some(new_owner));
+ }
+}
diff --git a/contracts/platform-governance/src/lib.rs b/contracts/platform-governance/src/lib.rs
index 240f8072..8981c0d4 100644
--- a/contracts/platform-governance/src/lib.rs
+++ b/contracts/platform-governance/src/lib.rs
@@ -54,9 +54,11 @@ pub enum ProposalType {
pub enum ProposalStatus {
Active,
Passed,
+ Queued,
Rejected,
Executed,
Expired,
+ Canceled,
}
/// Vote option for multi-choice proposals
@@ -163,11 +165,31 @@ fn vote_key(proposal_id: u64, voter: &Address) -> (Symbol, u64, Address) {
(symbol_short!("VOTE"), proposal_id, voter.clone())
}
+fn queued_proposal_key(proposal_id: u64) -> (Symbol, u64) {
+ (symbol_short!("QUEUED"), proposal_id)
+}
+
+/// Record of a proposal queued for timelock execution.
+#[contracttype]
+#[derive(Clone, Debug)]
+pub struct QueuedProposal {
+ /// Unique proposal ID
+ pub proposal_id: u64,
+ /// Timestamp when the proposal was queued
+ pub queued_at: u64,
+ /// Earliest execution timestamp (after timelock)
+ pub executable_at: u64,
+ /// True if the queued proposal was canceled by an admin
+ pub canceled: bool,
+}
+
// ── Constants ─────────────────────────────────────────────────────────────────
const DEFAULT_QUORUM_PERCENTAGE: u64 = 10; // 10%
const DEFAULT_TIMELOCK_SECONDS: u64 = 172800; // 48 hours
+#[allow(dead_code)]
const DEFAULT_PLATFORM_FEE: u64 = 5; // 5%
+#[allow(dead_code)]
const DEFAULT_MIN_PLANTING_BOND: i128 = 1_000_000; // 1M tokens
// ── Contract ──────────────────────────────────────────────────────────────────
@@ -257,12 +279,6 @@ impl PlatformGovernance {
.get(&proposal_count_key())
.unwrap_or(0);
- let timelock: u64 = env
- .storage()
- .instance()
- .get(&timelock_seconds_key())
- .expect("not initialized");
-
let now = env.ledger().timestamp();
// Initialize tally for each option
@@ -285,7 +301,7 @@ impl PlatformGovernance {
total_votes: 0,
created_at: now,
voting_ends_at: now + voting_period,
- executable_at: now + voting_period + timelock,
+ executable_at: 0,
};
env.storage()
@@ -389,12 +405,10 @@ impl PlatformGovernance {
if proposal.total_votes >= quorum_threshold {
// Check if there's a winning option (simple majority)
let mut max_votes = 0i128;
- let mut winning_option_id = 0u32;
for tally_entry in proposal.tally.iter() {
if tally_entry.votes > max_votes {
max_votes = tally_entry.votes;
- winning_option_id = tally_entry.option_id;
}
}
@@ -414,10 +428,10 @@ impl PlatformGovernance {
);
}
- /// Execute a passed proposal to update platform parameters.
+ /// Queue a passed proposal for execution after the timelock delay.
///
- /// `proposal_id` — proposal to execute
- pub fn execute(env: Env, proposal_id: u64) {
+ /// `proposal_id` — proposal that has passed voting
+ pub fn queue(env: Env, proposal_id: u64) {
Self::assert_not_paused(&env);
let mut proposal: ProposalRecord = env
@@ -430,8 +444,102 @@ impl PlatformGovernance {
panic!("proposal has not passed");
}
+ let timelock: u64 = env
+ .storage()
+ .instance()
+ .get(&timelock_seconds_key())
+ .expect("not initialized");
+
let now = env.ledger().timestamp();
- if now < proposal.executable_at {
+ let executable_at = now + timelock;
+
+ proposal.status = ProposalStatus::Queued;
+ proposal.executable_at = executable_at;
+ env.storage()
+ .persistent()
+ .set(&proposal_key(proposal_id), &proposal);
+
+ let queued = QueuedProposal {
+ proposal_id,
+ queued_at: now,
+ executable_at,
+ canceled: false,
+ };
+ env.storage()
+ .persistent()
+ .set(&queued_proposal_key(proposal_id), &queued);
+
+ env.events().publish(
+ (symbol_short!("proposal"), symbol_short!("queued")),
+ (proposal_id, executable_at),
+ );
+ }
+
+ /// Cancel a queued proposal. Admin only; intended for emergencies.
+ ///
+ /// `proposal_id` — queued proposal to cancel
+ pub fn cancel_queued_proposal(env: Env, proposal_id: u64) {
+ Self::require_admin(&env);
+
+ let mut proposal: ProposalRecord = env
+ .storage()
+ .persistent()
+ .get(&proposal_key(proposal_id))
+ .expect("proposal not found");
+
+ if proposal.status != ProposalStatus::Queued {
+ panic!("proposal is not queued");
+ }
+
+ proposal.status = ProposalStatus::Canceled;
+ env.storage()
+ .persistent()
+ .set(&proposal_key(proposal_id), &proposal);
+
+ let mut queued: QueuedProposal = env
+ .storage()
+ .persistent()
+ .get(&queued_proposal_key(proposal_id))
+ .expect("queued record not found");
+ queued.canceled = true;
+ env.storage()
+ .persistent()
+ .set(&queued_proposal_key(proposal_id), &queued);
+
+ env.events().publish(
+ (symbol_short!("proposal"), symbol_short!("canceled")),
+ proposal_id,
+ );
+ }
+
+ /// Execute a queued proposal to update platform parameters.
+ ///
+ /// `proposal_id` — proposal to execute
+ pub fn execute(env: Env, proposal_id: u64) {
+ Self::assert_not_paused(&env);
+
+ let mut proposal: ProposalRecord = env
+ .storage()
+ .persistent()
+ .get(&proposal_key(proposal_id))
+ .expect("proposal not found");
+
+ if proposal.status != ProposalStatus::Queued {
+ panic!("proposal is not queued");
+ }
+
+ let queued: QueuedProposal = env
+ .storage()
+ .persistent()
+ .get(&queued_proposal_key(proposal_id))
+ .expect("queued record not found");
+
+ if queued.canceled {
+ panic!("proposal has been canceled");
+ }
+
+ let now = env.ledger().timestamp();
+ if now < queued.executable_at {
panic!("timelock period has not elapsed");
}
@@ -548,6 +656,21 @@ impl PlatformGovernance {
.expect("not initialized")
}
+ /// Returns true if governance operations are paused.
+ pub fn is_paused(env: Env) -> bool {
+ env.storage()
+ .instance()
+ .get(&symbol_short!("PAUSED"))
+ .unwrap_or(false)
+ }
+
+ /// Returns the queued record for a proposal, if any.
+ pub fn get_queued_proposal(env: Env, proposal_id: u64) -> Option {
+ env.storage()
+ .persistent()
+ .get(&queued_proposal_key(proposal_id))
+ }
+
// ── Admin functions ───────────────────────────────────────────────────────
/// Update the quorum percentage. Admin only.
@@ -569,6 +692,10 @@ impl PlatformGovernance {
if new_timelock == 0 {
panic!("timelock must be > 0");
}
+ // Enforce a minimum timelock of 48 hours to protect the community.
+ if new_timelock < DEFAULT_TIMELOCK_SECONDS {
+ panic!("timelock must be at least 48 hours");
+ }
env.storage()
.instance()
.set(&timelock_seconds_key(), &new_timelock);
@@ -576,6 +703,18 @@ impl PlatformGovernance {
.publish((symbol_short!("timelock"),), new_timelock);
}
+ /// Pause or unpause governance operations. Admin only.
+ pub fn set_paused(env: Env, paused: bool) {
+ Self::require_admin(&env);
+ env.storage()
+ .instance()
+ .set(&symbol_short!("PAUSED"), &paused);
+ env.events().publish(
+ (symbol_short!("paused"),),
+ paused,
+ );
+ }
+
/// Directly set platform fee (emergency override). Admin only.
pub fn set_platform_fee(env: Env, new_fee: u64) {
Self::require_admin(&env);
@@ -816,9 +955,12 @@ mod tests {
}
#[test]
- fn test_execute_passed_proposal() {
+ fn test_queue_and_execute_passed_proposal() {
let (env, admin, _, _, client) = setup();
+ // Lower quorum so a single voter can pass the proposal.
+ client.update_quorum_percentage(&1);
+
let description_hash = String::from_str(&env, "hash123");
let proposal_type = ProposalType::PlatformFee;
@@ -828,22 +970,52 @@ mod tests {
description: String::from_str(&env, "Set fee to 10%"),
});
- client.create_proposal(&description_hash, &proposal_type, &options, &1, &admin); // 1 second voting period
-
- // Vote with admin (single vote for simplicity)
+ client.create_proposal(&description_hash, &proposal_type, &options, &1, &admin);
client.vote(&0, &1, &admin);
- // Wait for voting period and timelock to pass
- env.ledger().set_timestamp(env.ledger().timestamp() + 200000);
-
let proposal = client.get_proposal(&0);
- // Note: With single vote, quorum won't be met, so proposal won't pass
- // This test verifies the execution flow when quorum is met
- // In production, multiple voters would participate
+ assert!(matches!(proposal.status, ProposalStatus::Passed));
+
+ // Queue the proposal and verify the timelock is set.
+ client.queue(&0);
+ let queued = client.get_queued_proposal(&0).unwrap();
+ assert!(matches!(client.get_proposal(&0).status, ProposalStatus::Queued));
+ assert_eq!(queued.executable_at, env.ledger().timestamp() + DEFAULT_TIMELOCK_SECONDS);
+
+ // Advance ledger past the timelock.
+ env.ledger().set_timestamp(queued.executable_at + 1);
+
+ client.execute(&0);
+ let executed = client.get_proposal(&0);
+ assert!(matches!(executed.status, ProposalStatus::Executed));
}
#[test]
- #[should_panic(expected = "proposal has not passed")]
+ #[should_panic(expected = "timelock period has not elapsed")]
+ fn test_execute_before_timelock_rejected() {
+ let (env, admin, _, _, client) = setup();
+
+ client.update_quorum_percentage(&1);
+
+ let description_hash = String::from_str(&env, "hash123");
+ let proposal_type = ProposalType::PlatformFee;
+
+ let mut options = Vec::new(&env);
+ options.push_back(VoteOption {
+ option_id: 1,
+ description: String::from_str(&env, "Set fee to 10%"),
+ });
+
+ client.create_proposal(&description_hash, &proposal_type, &options, &1, &admin);
+ client.vote(&0, &1, &admin);
+ client.queue(&0);
+
+ // Try to execute immediately, before the timelock elapses.
+ client.execute(&0);
+ }
+
+ #[test]
+ #[should_panic(expected = "proposal is not queued")]
fn test_execute_failed_proposal_rejected() {
let (env, admin, _, _, client) = setup();
@@ -858,10 +1030,54 @@ mod tests {
client.create_proposal(&description_hash, &proposal_type, &options, &1, &admin);
- // Try to execute without meeting quorum
+ // Try to execute a proposal that never reached Queued status.
client.execute(&0);
}
+ #[test]
+ fn test_cancel_queued_proposal() {
+ let (env, admin, _, _, client) = setup();
+
+ client.update_quorum_percentage(&1);
+
+ let description_hash = String::from_str(&env, "hash123");
+ let proposal_type = ProposalType::PlatformFee;
+
+ let mut options = Vec::new(&env);
+ options.push_back(VoteOption {
+ option_id: 1,
+ description: String::from_str(&env, "Set fee to 10%"),
+ });
+
+ client.create_proposal(&description_hash, &proposal_type, &options, &1, &admin);
+ client.vote(&0, &1, &admin);
+ client.queue(&0);
+
+ client.cancel_queued_proposal(&0);
+ let canceled = client.get_proposal(&0);
+ assert!(matches!(canceled.status, ProposalStatus::Canceled));
+ assert!(client.get_queued_proposal(&0).unwrap().canceled);
+ }
+
+ #[test]
+ #[should_panic(expected = "timelock must be at least 48 hours")]
+ fn test_update_timelock_minimum_enforced() {
+ let (_, _admin, _, _, client) = setup();
+
+ client.update_timelock(&(DEFAULT_TIMELOCK_SECONDS - 1));
+ }
+
+ #[test]
+ fn test_set_paused() {
+ let (_, admin, _, _, client) = setup();
+
+ client.set_paused(&true);
+ assert!(client.is_paused());
+
+ client.set_paused(&false);
+ assert!(!client.is_paused());
+ }
+
#[test]
fn test_admin_set_platform_fee() {
let (_, _admin, _, _, client) = setup();
diff --git a/contracts/species-voting/src/lib.rs b/contracts/species-voting/src/lib.rs
index e6138cd7..6a5e68ac 100644
--- a/contracts/species-voting/src/lib.rs
+++ b/contracts/species-voting/src/lib.rs
@@ -91,23 +91,23 @@ fn admin_key() -> Symbol {
}
fn tree_token_key() -> Symbol {
- symbol_short!("TREE_TOKEN")
+ symbol_short!("TREE")
}
fn species_registry_key() -> Symbol {
- symbol_short!("SPECIES_REGISTRY")
+ symbol_short!("SPECIES")
}
fn proposal_count_key() -> Symbol {
- symbol_short!("PROPOSAL_COUNT")
+ symbol_short!("PROP_CNT")
}
fn voting_threshold_key() -> Symbol {
- symbol_short!("VOTE_THRESH")
+ symbol_short!("V_THRESH")
}
fn voting_period_key() -> Symbol {
- symbol_short!("VOTE_PERIOD")
+ symbol_short!("V_PERIOD")
}
fn proposal_key(id: u64) -> (Symbol, u64) {
@@ -169,14 +169,13 @@ impl SpeciesVoting {
/// `maturity_years` — years to biomass maturity
pub fn propose_species(
env: Env,
+ proposer: Address,
slug: Symbol,
name: String,
co2_scaled: i128,
maturity_years: u32,
) {
Self::assert_not_paused(&env);
-
- let proposer = env.invoker();
proposer.require_auth();
if co2_scaled <= 0 {
@@ -229,10 +228,8 @@ impl SpeciesVoting {
///
/// `proposal_id` — proposal to vote on
/// `vote_for` — true to vote for, false to vote against
- pub fn vote(env: Env, proposal_id: u64, vote_for: bool) {
+ pub fn vote(env: Env, voter: Address, proposal_id: u64, vote_for: bool) {
Self::assert_not_paused(&env);
-
- let voter = env.invoker();
voter.require_auth();
let mut proposal: ProposalRecord = env
@@ -472,12 +469,13 @@ mod tests {
#[test]
fn test_propose_species() {
- let (_, _, _, _, client) = setup();
+ let (env, _, _, _, client) = setup();
+ let proposer = Address::generate(&env);
let slug = Symbol::short("mahogany");
let name = String::from_str(&client.env, "Mahogany");
- client.propose_species(&slug, &name, &2500_i128, &25_u32);
+ client.propose_species(&proposer, &slug, &name, &2500_i128, &25_u32);
assert_eq!(client.proposal_count(), 1);
@@ -498,9 +496,9 @@ mod tests {
let slug = Symbol::short("oak");
let name = String::from_str(&env, "Oak");
- client.propose_species(&slug, &name, &3000_i128, &30_u32);
+ client.propose_species(&voter, &slug, &name, &3000_i128, &30_u32);
- client.vote(&0, &true);
+ client.vote(&voter, &0, &true);
let proposal = client.get_proposal(&0);
assert_eq!(proposal.votes_for, 500_000);
@@ -517,22 +515,23 @@ mod tests {
let slug = Symbol::short("pine");
let name = String::from_str(&env, "Pine");
- client.propose_species(&slug, &name, &2000_i128, &15_u32);
+ client.propose_species(&voter, &slug, &name, &2000_i128, &15_u32);
- client.vote(&0, &true);
- client.vote(&0, &false);
+ client.vote(&voter, &0, &true);
+ client.vote(&voter, &0, &false);
}
#[test]
#[should_panic(expected = "must hold TREE tokens to vote")]
fn test_vote_without_tokens_rejected() {
- let (_, _, _, _, client) = setup();
+ let (env, _, _, _, client) = setup();
+ let voter = Address::generate(&env);
let slug = Symbol::short("cedar");
let name = String::from_str(&client.env, "Cedar");
- client.propose_species(&slug, &name, &1800_i128, &20_u32);
+ client.propose_species(&voter, &slug, &name, &1800_i128, &20_u32);
- client.vote(&0, &true);
+ client.vote(&voter, &0, &true);
}
#[test]
@@ -546,13 +545,13 @@ mod tests {
let slug = Symbol::short("maple");
let name = String::from_str(&env, "Maple");
- client.propose_species(&slug, &name, &2800_i128, &25_u32);
+ client.propose_species(&voter1, &slug, &name, &2800_i128, &25_u32);
// Vote with voter1 (600k > 1M threshold, but need to test threshold logic)
// Actually threshold is 1M, so this won't pass yet
env.as_contract(&client.contract_id, || {
voter1.require_auth();
- client.vote(&0, &true);
+ client.vote(&voter1, &0, &true);
});
let proposal = client.get_proposal(&0);
@@ -568,9 +567,9 @@ mod tests {
let slug = Symbol::short("birch");
let name = String::from_str(&env, "Birch");
- client.propose_species(&slug, &name, &2200_i128, &20_u32);
+ client.propose_species(&voter, &slug, &name, &2200_i128, &20_u32);
- client.vote(&0, &true);
+ client.vote(&voter, &0, &true);
let proposal = client.get_proposal(&0);
if matches!(proposal.status, ProposalStatus::Passed) {
@@ -583,11 +582,12 @@ mod tests {
#[test]
#[should_panic(expected = "proposal has not passed")]
fn test_execute_failed_proposal_rejected() {
- let (_, _, _, _, client) = setup();
+ let (env, _, _, _, client) = setup();
+ let proposer = Address::generate(&env);
let slug = Symbol::short("elm");
let name = String::from_str(&client.env, "Elm");
- client.propose_species(&slug, &name, &2400_i128, &22_u32);
+ client.propose_species(&proposer, &slug, &name, &2400_i128, &22_u32);
client.execute_proposal(&0);
}
diff --git a/contracts/subscription-sponsorship/src/lib.rs b/contracts/subscription-sponsorship/src/lib.rs
index dce26fed..a69ba73f 100644
--- a/contracts/subscription-sponsorship/src/lib.rs
+++ b/contracts/subscription-sponsorship/src/lib.rs
@@ -28,7 +28,7 @@
//! tree escrows in the tree-escrow contract using those funds.
use soroban_sdk::{
- contract, contractimpl, contracttype, symbol_short, token, Address, Env, Vec,
+ contract, contractimpl, contracttype, symbol_short, token, Address, Env, IntoVal, Vec,
};
// ── Constants ─────────────────────────────────────────────────────────────────
@@ -224,31 +224,24 @@ impl SubscriptionSponsorship {
rec.total_trees_sponsored += rec.trees_per_cycle;
// Try to lock the next cycle's amount from the sponsor.
- // If the transfer fails (insufficient balance), cancel the subscription
+ // If the sponsor has insufficient balance, cancel the subscription
// gracefully rather than panicking.
- let lock_next = || {
+ let sponsor_balance = token::Client::new(&env, &rec.token).balance(&rec.sponsor);
+ if sponsor_balance >= rec.amount_per_cycle {
token::Client::new(&env, &rec.token).transfer(
&rec.sponsor,
&env.current_contract_address(),
&rec.amount_per_cycle,
);
- };
-
- // `env.try()` catches panics from the closure and returns `Result`.
- // Since `transfer()` returns `()`, the result is `Result<(), Error>`.
- match env.try(lock_next) {
- Ok(_) => {
- rec.next_processing = now + rec.interval_seconds;
- // Keep status as Active
- }
- Err(_) => {
- // Sponsor doesn't have enough funds — cancel gracefully
- rec.status = SubscriptionStatus::Cancelled;
- env.events().publish(
- (symbol_short!("sub"), symbol_short!("cancel")),
- (subscription_id, rec.sponsor.clone(), symbol_short!("no_funds")),
- );
- }
+ rec.next_processing = now + rec.interval_seconds;
+ // Keep status as Active
+ } else {
+ // Sponsor doesn't have enough funds — cancel gracefully
+ rec.status = SubscriptionStatus::Cancelled;
+ env.events().publish(
+ (symbol_short!("sub"), symbol_short!("cancel")),
+ (subscription_id, rec.sponsor.clone(), symbol_short!("no_funds")),
+ );
}
env.storage().persistent().set(&key, &rec);
diff --git a/contracts/tree-escrow/src/lib.rs b/contracts/tree-escrow/src/lib.rs
index 8c0c1a88..bfc6bf8a 100644
--- a/contracts/tree-escrow/src/lib.rs
+++ b/contracts/tree-escrow/src/lib.rs
@@ -33,7 +33,8 @@
//! • `get_qr_hash` — retrieve the stored hash for off-chain label checking.
use soroban_sdk::{
- contract, contractimpl, contracttype, symbol_short, token, Address, BytesN, Env, Vec,
+ contract, contractimpl, contracttype, panic_with_error, symbol_short, token, Address, BytesN,
+ Env, Vec,
};
use harvesta_errors::HarvestaError;
@@ -55,6 +56,9 @@ const PROGRESS_STREAM_COUNT: u32 = 5;
/// 6 months in seconds (approx 26 weeks)
const SIX_MONTHS_SECS: u64 = 60 * 60 * 24 * 7 * 26;
+/// 1 year in seconds (approx 52 weeks)
+const ONE_YEAR_SECS: u64 = 60 * 60 * 24 * 7 * 52;
+
/// Window in which a sponsor may challenge a verification outcome (#469)
const DISPUTE_WINDOW_SECS: u64 = 60 * 60 * 24 * 7;
@@ -197,6 +201,23 @@ pub struct PlanterReputation {
pub average_rating: u32, // Calculated as sum / total (scaled to 0-100)
}
+/// Classification of a payout recorded for a planter.
+#[contracttype]
+#[derive(Clone, Debug, PartialEq)]
+pub enum PayoutType {
+ Tranche2,
+}
+
+/// A single payout entry stored in the planter's payout history.
+#[contracttype]
+#[derive(Clone, Debug)]
+pub struct Payout {
+ pub planter: Address,
+ pub amount: i128,
+ pub payout_type: PayoutType,
+ pub timestamp: u64,
+}
+
// ── Storage keys ──────────────────────────────────────────────────────────────
#[contracttype]
@@ -221,6 +242,14 @@ enum DataKey {
Dispute(u64),
/// DAO members authorised to arbitrate disputes
DaoMembers,
+ /// Contract-wide emergency pause flag
+ Paused,
+ /// Sponsor rating keyed by (sponsor, farmer)
+ SponsorRating(Address, Address),
+ /// Aggregated planter reputation keyed by farmer
+ PlanterReputation(Address),
+ /// Payout history keyed by planter
+ PayoutHistory(Address),
}
// ── Contract ──────────────────────────────────────────────────────────────────
@@ -451,7 +480,7 @@ impl TreeEscrow {
planting_proof: empty_hash.clone(),
survival_proof: empty_hash.clone(),
survival_rate_percent: 0,
- year_proof: empty_hash,
+ year_proof: empty_hash.clone(),
},
);
env.events()
@@ -463,6 +492,14 @@ impl TreeEscrow {
/// Admin-verified planting: releases Tranche 1 (30%) and mints TREE rewards.
pub fn verify_planting(
+ env: Env,
+ farmer: Address,
+ proof_hash: BytesN<32>,
+ verified_tree_count: i128,
+ ) {
+ Self::verify_progress(env, farmer, proof_hash, verified_tree_count)
+ }
+
/// Admin-verified progress update: streams 10% of the escrow to the planter.
///
/// May be called up to 5 times per escrow (each releasing exactly 10% of
@@ -522,7 +559,7 @@ impl TreeEscrow {
rec.tree_tokens_minted = tree_tokens;
rec.status = EscrowStatus::Planted;
rec.planted_at = env.ledger().timestamp();
- rec.planting_proof = proof_hash;
+ rec.planting_proof = proof_hash.clone();
env.events()
.publish((symbol_short!("treemint"), recipient), tree_tokens);
@@ -665,7 +702,7 @@ impl TreeEscrow {
env.storage().persistent().set(&key, &rec);
env.events()
- .publish((symbol_short!("year_milestone"), farmer), tranche3);
+ .publish((symbol_short!("year_ms"), farmer), tranche3);
}
pub fn refund(env: Env, farmer: Address) {
@@ -762,7 +799,7 @@ impl TreeEscrow {
rep.total_ratings += 1;
rep.sum_ratings += rating as u128;
- rep.average_rating = (rep.sum_ratings * 20) / rep.total_ratings as u128; // Scale to 0-100 (5 stars * 20 = 100)
+ rep.average_rating = ((rep.sum_ratings * 20) / rep.total_ratings as u128) as u32; // Scale to 0-100 (5 stars * 20 = 100)
env.storage().persistent().set(&rep_key, &rep);
@@ -1251,9 +1288,7 @@ impl TreeEscrow {
let mut unit = 1i128;
let mut i = 0u32;
while i < decimals {
- unit = unit
- .checked_mul(10)
- .unwrap_or_else(|| panic_with_error!(env, HarvestaError::TokenUnitOverflow));
+ unit = unit.checked_mul(10).expect("token unit overflow");
i += 1;
}
unit
@@ -1277,6 +1312,20 @@ impl TreeEscrow {
payouts.push_back(payout);
env.storage().persistent().set(&key, &payouts);
}
+
+ fn min_density(env: &Env) -> i128 {
+ env.storage()
+ .instance()
+ .get(&DataKey::MinDensity)
+ .expect("min_density not initialized")
+ }
+
+ fn job_size_threshold(env: &Env) -> i128 {
+ env.storage()
+ .instance()
+ .get(&DataKey::JobSizeThreshold)
+ .expect("job_size_threshold not initialized")
+ }
}
// ── Tests ─────────────────────────────────────────────────────────────────────
@@ -1465,7 +1514,6 @@ mod tests {
.deposit(&ctx.donor, &ctx.farmer, &ctx.token, &10_000, &42, &5);
ctx.client
.verify_planting(&ctx.farmer, &proof(&ctx.env, 1), &42);
- .deposit(&ctx.donor, &ctx.farmer, &ctx.token, &10_000, &42);
for i in 0..5 {
ctx.client
.verify_progress(&ctx.farmer, &proof(&ctx.env, i as u8 + 1), &42);
@@ -1483,7 +1531,6 @@ mod tests {
.deposit(&ctx.donor, &ctx.farmer, &ctx.token, &10_000, &42, &5);
ctx.client
.verify_planting(&ctx.farmer, &proof(&ctx.env, 1), &42);
- .deposit(&ctx.donor, &ctx.farmer, &ctx.token, &10_000, &42);
for i in 0..5 {
ctx.client
.verify_progress(&ctx.farmer, &proof(&ctx.env, i as u8 + 1), &42);
@@ -1502,7 +1549,6 @@ mod tests {
.deposit(&ctx.donor, &ctx.farmer, &ctx.token, &10_000, &42, &5);
ctx.client
.verify_planting(&ctx.farmer, &proof(&ctx.env, 1), &42);
- .deposit(&ctx.donor, &ctx.farmer, &ctx.token, &10_000, &42);
for i in 0..5 {
ctx.client
.verify_progress(&ctx.farmer, &proof(&ctx.env, i as u8 + 1), &42);
@@ -1520,7 +1566,6 @@ mod tests {
.with_mut(|l| l.timestamp += ONE_YEAR_SECS - SIX_MONTHS_SECS + 1);
ctx.client
.verify_year_milestone(&ctx.farmer, &proof(&ctx.env, 3));
- .verify_survival(&ctx.farmer, &proof(&ctx.env, 6), &55);
assert_eq!(
ctx.client.get_record(&ctx.farmer).unwrap().status,
EscrowStatus::Completed
@@ -1533,7 +1578,6 @@ mod tests {
let ctx = setup();
ctx.client
.deposit(&ctx.donor, &ctx.farmer, &ctx.token, &10_000, &42, &5);
- .deposit(&ctx.donor, &ctx.farmer, &ctx.token, &10_000, &42);
for i in 0..5 {
ctx.client
.verify_progress(&ctx.farmer, &proof(&ctx.env, i as u8 + 1), &42);
@@ -2300,17 +2344,4 @@ mod tests {
}
}
- fn min_density(env: &Env) -> i128 {
- env.storage()
- .instance()
- .get(&DataKey::MinDensity)
- .expect(" not initialized\)
- }
-
- fn job_size_threshold(env: &Env) -> i128 {
- env.storage()
- .instance()
- .get(&DataKey::JobSizeThreshold)
- .expect(\not initialized\)
- }
diff --git a/contracts/tree-token/src/lib.rs b/contracts/tree-token/src/lib.rs
index e0f5669c..dfb3f315 100644
--- a/contracts/tree-token/src/lib.rs
+++ b/contracts/tree-token/src/lib.rs
@@ -13,10 +13,19 @@
//!
//! All state-changing functions check the pause flag (see #323 integration).
//! The admin can pause/unpause and update the oracle address.
+//!
+//! # Storage optimization
+//!
+//! Instance storage is collapsed into a single `Config` struct keyed by
+//! `DataKey::Config`, reducing the number of instance ledger entries from
+//! four to one. Burn record indices use `u32` instead of `u64`, and the
+//! persistent key is a typed `DataKey::BurnRecord(u32)` rather than a
+//! `(Symbol, u64)` tuple. These changes lower the on-chain ledger footprint
+//! and the storage fees paid by users.
use soroban_sdk::{
contract, contractimpl, contracttype, panic_with_error, symbol_short, token, Address, Env,
- IntoVal, Symbol,
+ Symbol,
};
use harvesta_errors::HarvestaError;
@@ -36,6 +45,30 @@ pub struct BurnRecord {
pub burned_at: u64,
}
+/// Contract configuration stored in a single instance entry.
+#[contracttype]
+#[derive(Clone, Debug)]
+pub struct Config {
+ /// Admin address
+ pub admin: Address,
+ /// Address of the deployed TREE SAC token contract
+ pub token: Address,
+ /// Pause flag
+ pub paused: bool,
+ /// Total number of recorded burns
+ pub burn_count: u32,
+}
+
+/// Storage keys.
+#[contracttype]
+#[derive(Clone, Debug)]
+pub enum DataKey {
+ /// Single config entry (admin, token, paused, burn_count)
+ Config,
+ /// Burn record by index
+ BurnRecord(u32),
+}
+
// ── Contract ──────────────────────────────────────────────────────────────────
#[contract]
@@ -48,22 +81,19 @@ impl TreeToken {
/// `admin` — multi-sig admin address (pause/unpause, oracle updates)
/// `tree_token` — address of the deployed TREE SAC token contract
pub fn initialize(env: Env, admin: Address, tree_token: Address) {
- if env.storage().instance().has(&symbol_short!("ADMIN")) {
+ if env.storage().instance().has(&DataKey::Config) {
panic_with_error!(&env, HarvestaError::AlreadyInitialized);
}
contract_utils::assert_whitelisted(&env, &tree_token);
- env.storage()
- .instance()
- .set(&symbol_short!("ADMIN"), &admin);
- env.storage()
- .instance()
- .set(&symbol_short!("TOKEN"), &tree_token);
- env.storage()
- .instance()
- .set(&symbol_short!("PAUSED"), &false);
- env.storage()
- .instance()
- .set(&symbol_short!("BURNCOUNT"), &0u64);
+ env.storage().instance().set(
+ &DataKey::Config,
+ &Config {
+ admin,
+ token: tree_token,
+ paused: false,
+ burn_count: 0,
+ },
+ );
}
/// Burn `amount` TREE tokens from `burner`'s balance to claim a carbon offset.
@@ -80,23 +110,13 @@ impl TreeToken {
panic_with_error!(&env, HarvestaError::BurnAmountMustBePositive);
}
- let tree_token: Address = env
- .storage()
- .instance()
- .get(&symbol_short!("TOKEN"))
- .unwrap_or_else(|| panic_with_error!(&env, HarvestaError::NotInitialized));
+ let mut config = Self::config(&env);
- contract_utils::assert_whitelisted(&env, &tree_token);
+ contract_utils::assert_whitelisted(&env, &config.token);
// Burn tokens from burner's balance via SAC interface
- token::Client::new(&env, &tree_token).burn(&burner, &amount);
+ token::Client::new(&env, &config.token).burn(&burner, &amount);
// Record the burn on-chain for ESG audit
- let count: u64 = env
- .storage()
- .instance()
- .get(&symbol_short!("BURNCOUNT"))
- .unwrap_or(0);
-
let record = BurnRecord {
burner: burner.clone(),
token_count: amount,
@@ -104,11 +124,13 @@ impl TreeToken {
burned_at: env.ledger().timestamp(),
};
- let key = Self::burn_key(&env, count);
+ let key = DataKey::BurnRecord(config.burn_count);
env.storage().persistent().set(&key, &record);
- env.storage()
- .instance()
- .set(&symbol_short!("BURNCOUNT"), &count.checked_add(1).expect("burn count overflow"));
+ config.burn_count = config
+ .burn_count
+ .checked_add(1)
+ .expect("burn count overflow");
+ env.storage().instance().set(&DataKey::Config, &config);
// Emit TokenBurned event — primary ESG audit signal
env.events()
@@ -116,16 +138,13 @@ impl TreeToken {
}
/// Returns the burn record at sequential index `idx`, or None.
- pub fn get_burn_record(env: Env, idx: u64) -> Option {
- env.storage().persistent().get(&Self::burn_key(&env, idx))
+ pub fn get_burn_record(env: Env, idx: u32) -> Option {
+ env.storage().persistent().get(&DataKey::BurnRecord(idx))
}
/// Returns the total number of burn operations recorded.
- pub fn burn_count(env: Env) -> u64 {
- env.storage()
- .instance()
- .get(&symbol_short!("BURNCOUNT"))
- .unwrap_or(0)
+ pub fn burn_count(env: Env) -> u32 {
+ Self::config(&env).burn_count
}
// ── Admin functions ───────────────────────────────────────────────────────
@@ -133,9 +152,9 @@ impl TreeToken {
/// Pause all state-changing functions. Admin multi-sig only.
pub fn pause(env: Env) {
Self::require_admin(&env);
- env.storage()
- .instance()
- .set(&symbol_short!("PAUSED"), &true);
+ let mut config = Self::config(&env);
+ config.paused = true;
+ env.storage().instance().set(&DataKey::Config, &config);
env.events()
.publish((symbol_short!("paused"),), env.ledger().timestamp());
}
@@ -143,39 +162,33 @@ impl TreeToken {
/// Unpause the contract. Admin multi-sig only.
pub fn unpause(env: Env) {
Self::require_admin(&env);
- env.storage()
- .instance()
- .set(&symbol_short!("PAUSED"), &false);
+ let mut config = Self::config(&env);
+ config.paused = false;
+ env.storage().instance().set(&DataKey::Config, &config);
env.events()
.publish((symbol_short!("unpaused"),), env.ledger().timestamp());
}
/// Returns true if the contract is currently paused.
pub fn is_paused(env: Env) -> bool {
+ Self::config(&env).paused
+ }
+
+ // ── Internal ──────────────────────────────────────────────────────────────
+
+ fn config(env: &Env) -> Config {
env.storage()
.instance()
- .get(&symbol_short!("PAUSED"))
- .unwrap_or(false)
+ .get(&DataKey::Config)
+ .unwrap_or_else(|| panic_with_error!(env, HarvestaError::NotInitialized))
}
- // ── internal ──────────────────────────────────────────────────────────────
-
fn require_admin(env: &Env) {
- let admin: Address = env
- .storage()
- .instance()
- .get(&symbol_short!("ADMIN"))
- .unwrap_or_else(|| panic_with_error!(env, HarvestaError::NotInitialized));
- admin.require_auth();
+ Self::config(env).admin.require_auth();
}
fn assert_not_paused(env: &Env) {
- let paused: bool = env
- .storage()
- .instance()
- .get(&symbol_short!("PAUSED"))
- .unwrap_or(false);
- if paused {
+ if Self::config(env).paused {
panic_with_error!(env, HarvestaError::ContractPaused);
}
}
@@ -203,10 +216,6 @@ impl TreeToken {
pub fn assert_whitelisted(env: Env, addr: Address) {
contract_utils::assert_whitelisted(&env, &addr);
}
-
- fn burn_key(env: &Env, idx: u64) -> soroban_sdk::Val {
- (symbol_short!("BURN"), idx).into_val(env)
- }
}
// ── Tests ─────────────────────────────────────────────────────────────────────