diff --git a/smart-contract/contracts/src/error.rs b/smart-contract/contracts/src/error.rs
index 5dd5b1d5..5399948a 100644
--- a/smart-contract/contracts/src/error.rs
+++ b/smart-contract/contracts/src/error.rs
@@ -59,4 +59,20 @@ pub enum Error {
InvalidThreshold = 66,
TooManySigners = 67,
DuplicateSigner = 68,
+
+ // --- Sustainability (70-80) ---
+ /// No sustainability record exists for this product.
+ SustainabilityNotFound = 70,
+ /// Carbon footprint value is negative.
+ InvalidCarbonData = 71,
+ /// Water usage value is negative.
+ InvalidWaterData = 72,
+ /// Renewable energy percentage is out of range (must be 0–100).
+ InvalidRenewableEnergyData = 73,
+ /// Waste-recycled percentage is out of range (must be 0–100).
+ InvalidWasteData = 74,
+ /// Record has already been verified and cannot be updated.
+ SustainabilityAlreadyVerified = 75,
+ /// Operation requires a verified sustainability record.
+ SustainabilityClaimUnverified = 76,
}
diff --git a/smart-contract/contracts/src/lib.rs b/smart-contract/contracts/src/lib.rs
index 716a4489..a6015ec1 100644
--- a/smart-contract/contracts/src/lib.rs
+++ b/smart-contract/contracts/src/lib.rs
@@ -40,6 +40,8 @@ mod stats;
mod tracking;
#[cfg(not(target_arch = "wasm32"))]
mod upgrade;
+#[cfg(not(target_arch = "wasm32"))]
+mod sustainability;
#[cfg(test)]
mod load_tests;
@@ -76,3 +78,5 @@ pub use stats::*;
pub use tracking::*;
#[cfg(not(target_arch = "wasm32"))]
pub use upgrade::*;
+#[cfg(not(target_arch = "wasm32"))]
+pub use sustainability::*;
diff --git a/smart-contract/contracts/src/storage.rs b/smart-contract/contracts/src/storage.rs
index 4d4d2661..3a95f8df 100644
--- a/smart-contract/contracts/src/storage.rs
+++ b/smart-contract/contracts/src/storage.rs
@@ -1,7 +1,7 @@
use soroban_sdk::{Address, Env, String, Symbol, Vec};
use crate::storage_contract::StorageContract;
-use crate::types::{Product, TrackingEvent};
+use crate::types::{Product, SustainabilityRecord, TrackingEvent};
pub fn get_auth_contract(env: &Env) -> Option
{
StorageContract::get_auth_contract(env)
@@ -178,3 +178,17 @@ pub fn remove_from_search_index(env: &Env, keyword: String, product_id: &String)
put_search_index(env, &keyword, &ids);
}
}
+
+// ─── Sustainability ──────────────────────────────────────────────────────────
+
+pub fn put_sustainability(env: &Env, product_id: &String, record: &SustainabilityRecord) {
+ StorageContract::put_sustainability(env, product_id, record)
+}
+
+pub fn get_sustainability(env: &Env, product_id: &String) -> Option {
+ StorageContract::get_sustainability(env, product_id)
+}
+
+pub fn has_sustainability(env: &Env, product_id: &String) -> bool {
+ StorageContract::has_sustainability(env, product_id)
+}
diff --git a/smart-contract/contracts/src/storage_contract.rs b/smart-contract/contracts/src/storage_contract.rs
index 3b5cce89..de5040a7 100644
--- a/smart-contract/contracts/src/storage_contract.rs
+++ b/smart-contract/contracts/src/storage_contract.rs
@@ -1,6 +1,6 @@
use soroban_sdk::{Address, Env, String, Symbol, Vec};
-use crate::types::{DataKey, Product, TrackingEvent};
+use crate::types::{DataKey, Product, SustainabilityRecord, TrackingEvent};
pub struct StorageContract;
@@ -279,6 +279,30 @@ impl StorageContract {
.instance()
.set(&Self::active_products_key(), &count);
}
+
+ // ─── Sustainability ──────────────────────────────────────────────────────
+
+ pub fn sustainability_key(product_id: &String) -> DataKey {
+ DataKey::Sustainability(product_id.clone())
+ }
+
+ pub fn put_sustainability(env: &Env, product_id: &String, record: &SustainabilityRecord) {
+ env.storage()
+ .instance()
+ .set(&Self::sustainability_key(product_id), record);
+ }
+
+ pub fn get_sustainability(env: &Env, product_id: &String) -> Option {
+ env.storage()
+ .instance()
+ .get(&Self::sustainability_key(product_id))
+ }
+
+ pub fn has_sustainability(env: &Env, product_id: &String) -> bool {
+ env.storage()
+ .instance()
+ .has(&Self::sustainability_key(product_id))
+ }
}
#[cfg(test)]
diff --git a/smart-contract/contracts/src/sustainability.rs b/smart-contract/contracts/src/sustainability.rs
new file mode 100644
index 00000000..4b99d4ae
--- /dev/null
+++ b/smart-contract/contracts/src/sustainability.rs
@@ -0,0 +1,391 @@
+use soroban_sdk::{contract, contractimpl, Address, BytesN, Env, String, Symbol};
+
+use crate::error::Error;
+use crate::storage;
+use crate::types::{SustainabilityRecord, SustainabilityStatus};
+
+fn require_admin(env: &Env, caller: &Address) -> Result<(), Error> {
+ let admin = storage::get_admin(env).ok_or(Error::NotInitialized)?;
+ if &admin != caller {
+ return Err(Error::Unauthorized);
+ }
+ caller.require_auth();
+ Ok(())
+}
+
+fn validate_record(
+ carbon_footprint_g: i128,
+ water_usage_ml: i128,
+ renewable_energy_pct: u32,
+ waste_recycled_pct: u32,
+) -> Result<(), Error> {
+ if carbon_footprint_g < 0 {
+ return Err(Error::InvalidCarbonData);
+ }
+ if water_usage_ml < 0 {
+ return Err(Error::InvalidWaterData);
+ }
+ if renewable_energy_pct > 100 {
+ return Err(Error::InvalidRenewableEnergyData);
+ }
+ if waste_recycled_pct > 100 {
+ return Err(Error::InvalidWasteData);
+ }
+ Ok(())
+}
+
+/// Contract for recording and verifying supply-chain sustainability claims.
+///
+/// Flow:
+/// 1. A product owner calls `record_sustainability` to submit environmental data.
+/// 2. An admin calls `verify_sustainability` (anchoring a certificate hash) or
+/// `reject_sustainability` to update the record status.
+/// 3. Anyone can call `get_sustainability` to read the current record.
+#[contract]
+pub struct SustainabilityContract;
+
+#[contractimpl]
+impl SustainabilityContract {
+ /// Submit a sustainability record for a product.
+ ///
+ /// Fails if a `Verified` record already exists for this product.
+ /// Overwrites a `Pending` or `Rejected` record.
+ pub fn record_sustainability(
+ env: Env,
+ caller: Address,
+ product_id: String,
+ carbon_footprint_g: i128,
+ water_usage_ml: i128,
+ renewable_energy_pct: u32,
+ waste_recycled_pct: u32,
+ labor_compliance_hash: BytesN<32>,
+ certificate_hash: Option>,
+ ) -> Result<(), Error> {
+ caller.require_auth();
+
+ validate_record(
+ carbon_footprint_g,
+ water_usage_ml,
+ renewable_energy_pct,
+ waste_recycled_pct,
+ )?;
+
+ // Prevent overwriting an already-verified record.
+ if let Some(existing) = storage::get_sustainability(&env, &product_id) {
+ if existing.status == SustainabilityStatus::Verified {
+ return Err(Error::SustainabilityAlreadyVerified);
+ }
+ }
+
+ let record = SustainabilityRecord {
+ carbon_footprint_g,
+ water_usage_ml,
+ renewable_energy_pct,
+ waste_recycled_pct,
+ labor_compliance_hash,
+ certificate_hash,
+ status: SustainabilityStatus::Pending,
+ recorded_at: env.ledger().timestamp(),
+ recorded_by: caller.clone(),
+ verified_by: None,
+ verified_at: 0,
+ };
+
+ storage::put_sustainability(&env, &product_id, &record);
+
+ env.events().publish(
+ (Symbol::new(&env, "sustainability"), Symbol::new(&env, "recorded")),
+ (product_id, caller),
+ );
+
+ Ok(())
+ }
+
+ /// Mark a pending sustainability record as verified (admin only).
+ ///
+ /// `certificate_hash` is the hash of the third-party verification document
+ /// that is stored off-chain and anchored here for auditability.
+ pub fn verify_sustainability(
+ env: Env,
+ admin: Address,
+ product_id: String,
+ certificate_hash: BytesN<32>,
+ ) -> Result<(), Error> {
+ require_admin(&env, &admin)?;
+
+ let mut record = storage::get_sustainability(&env, &product_id)
+ .ok_or(Error::SustainabilityNotFound)?;
+
+ if record.status == SustainabilityStatus::Verified {
+ return Err(Error::SustainabilityAlreadyVerified);
+ }
+
+ record.status = SustainabilityStatus::Verified;
+ record.certificate_hash = Some(certificate_hash);
+ record.verified_by = Some(admin.clone());
+ record.verified_at = env.ledger().timestamp();
+
+ storage::put_sustainability(&env, &product_id, &record);
+
+ env.events().publish(
+ (Symbol::new(&env, "sustainability"), Symbol::new(&env, "verified")),
+ (product_id, admin),
+ );
+
+ Ok(())
+ }
+
+ /// Mark a pending sustainability record as rejected (admin only).
+ pub fn reject_sustainability(
+ env: Env,
+ admin: Address,
+ product_id: String,
+ ) -> Result<(), Error> {
+ require_admin(&env, &admin)?;
+
+ let mut record = storage::get_sustainability(&env, &product_id)
+ .ok_or(Error::SustainabilityNotFound)?;
+
+ if record.status == SustainabilityStatus::Verified {
+ return Err(Error::SustainabilityAlreadyVerified);
+ }
+
+ record.status = SustainabilityStatus::Rejected;
+ record.verified_by = Some(admin.clone());
+ record.verified_at = env.ledger().timestamp();
+
+ storage::put_sustainability(&env, &product_id, &record);
+
+ env.events().publish(
+ (Symbol::new(&env, "sustainability"), Symbol::new(&env, "rejected")),
+ (product_id, admin),
+ );
+
+ Ok(())
+ }
+
+ /// Retrieve the sustainability record for a product.
+ pub fn get_sustainability(
+ env: Env,
+ product_id: String,
+ ) -> Result {
+ storage::get_sustainability(&env, &product_id)
+ .ok_or(Error::SustainabilityNotFound)
+ }
+}
+
+#[cfg(test)]
+mod sustainability_tests {
+ use super::*;
+ use soroban_sdk::testutils::Address as _;
+ use soroban_sdk::{Address, BytesN, Env};
+
+ fn setup() -> (Env, Address, Address) {
+ let env = Env::default();
+ env.mock_all_auths();
+ let admin = Address::generate(&env);
+ let contract_id = env.register_contract(None, SustainabilityContract);
+ env.as_contract(&contract_id, || {
+ storage::set_admin(&env, &admin);
+ });
+ let owner = Address::generate(&env);
+ (env, admin, owner)
+ }
+
+ fn zero_hash(env: &Env) -> BytesN<32> {
+ BytesN::from_array(env, &[0u8; 32])
+ }
+
+ fn one_hash(env: &Env) -> BytesN<32> {
+ BytesN::from_array(env, &[1u8; 32])
+ }
+
+ #[test]
+ fn test_record_and_get() {
+ let (env, _admin, owner) = setup();
+ let contract_id = env.register_contract(None, SustainabilityContract);
+ let client = SustainabilityContractClient::new(&env, &contract_id);
+
+ env.as_contract(&contract_id, || {
+ storage::set_admin(&env, &_admin);
+ });
+
+ let product_id = soroban_sdk::String::from_str(&env, "P001");
+
+ client
+ .record_sustainability(
+ &owner,
+ &product_id,
+ &5000_i128,
+ &10000_i128,
+ &80_u32,
+ &60_u32,
+ &zero_hash(&env),
+ &None,
+ )
+ .unwrap();
+
+ let record = client.get_sustainability(&product_id).unwrap();
+ assert_eq!(record.status, SustainabilityStatus::Pending);
+ assert_eq!(record.carbon_footprint_g, 5000);
+ assert_eq!(record.renewable_energy_pct, 80);
+ }
+
+ #[test]
+ fn test_verify_sustainability() {
+ let (env, admin, owner) = setup();
+ let contract_id = env.register_contract(None, SustainabilityContract);
+ let client = SustainabilityContractClient::new(&env, &contract_id);
+
+ env.as_contract(&contract_id, || {
+ storage::set_admin(&env, &admin);
+ });
+
+ let product_id = soroban_sdk::String::from_str(&env, "P002");
+
+ client
+ .record_sustainability(
+ &owner,
+ &product_id,
+ &1000_i128,
+ &500_i128,
+ &100_u32,
+ &100_u32,
+ &zero_hash(&env),
+ &None,
+ )
+ .unwrap();
+
+ client
+ .verify_sustainability(&admin, &product_id, &one_hash(&env))
+ .unwrap();
+
+ let record = client.get_sustainability(&product_id).unwrap();
+ assert_eq!(record.status, SustainabilityStatus::Verified);
+ assert_eq!(record.certificate_hash, Some(one_hash(&env)));
+ }
+
+ #[test]
+ fn test_reject_sustainability() {
+ let (env, admin, owner) = setup();
+ let contract_id = env.register_contract(None, SustainabilityContract);
+ let client = SustainabilityContractClient::new(&env, &contract_id);
+
+ env.as_contract(&contract_id, || {
+ storage::set_admin(&env, &admin);
+ });
+
+ let product_id = soroban_sdk::String::from_str(&env, "P003");
+
+ client
+ .record_sustainability(
+ &owner,
+ &product_id,
+ &0_i128,
+ &0_i128,
+ &0_u32,
+ &0_u32,
+ &zero_hash(&env),
+ &None,
+ )
+ .unwrap();
+
+ client.reject_sustainability(&admin, &product_id).unwrap();
+
+ let record = client.get_sustainability(&product_id).unwrap();
+ assert_eq!(record.status, SustainabilityStatus::Rejected);
+ }
+
+ #[test]
+ fn test_invalid_carbon_data() {
+ let (env, _admin, owner) = setup();
+ let contract_id = env.register_contract(None, SustainabilityContract);
+ let client = SustainabilityContractClient::new(&env, &contract_id);
+
+ env.as_contract(&contract_id, || {
+ storage::set_admin(&env, &_admin);
+ });
+
+ let product_id = soroban_sdk::String::from_str(&env, "P004");
+
+ let result = client.try_record_sustainability(
+ &owner,
+ &product_id,
+ &(-1_i128),
+ &0_i128,
+ &0_u32,
+ &0_u32,
+ &zero_hash(&env),
+ &None,
+ );
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_invalid_percentage() {
+ let (env, _admin, owner) = setup();
+ let contract_id = env.register_contract(None, SustainabilityContract);
+ let client = SustainabilityContractClient::new(&env, &contract_id);
+
+ env.as_contract(&contract_id, || {
+ storage::set_admin(&env, &_admin);
+ });
+
+ let product_id = soroban_sdk::String::from_str(&env, "P005");
+
+ let result = client.try_record_sustainability(
+ &owner,
+ &product_id,
+ &0_i128,
+ &0_i128,
+ &101_u32,
+ &0_u32,
+ &zero_hash(&env),
+ &None,
+ );
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_cannot_overwrite_verified() {
+ let (env, admin, owner) = setup();
+ let contract_id = env.register_contract(None, SustainabilityContract);
+ let client = SustainabilityContractClient::new(&env, &contract_id);
+
+ env.as_contract(&contract_id, || {
+ storage::set_admin(&env, &admin);
+ });
+
+ let product_id = soroban_sdk::String::from_str(&env, "P006");
+
+ client
+ .record_sustainability(
+ &owner,
+ &product_id,
+ &0_i128,
+ &0_i128,
+ &0_u32,
+ &0_u32,
+ &zero_hash(&env),
+ &None,
+ )
+ .unwrap();
+
+ client
+ .verify_sustainability(&admin, &product_id, &one_hash(&env))
+ .unwrap();
+
+ // Attempting to re-record a verified entry must fail.
+ let result = client.try_record_sustainability(
+ &owner,
+ &product_id,
+ &0_i128,
+ &0_i128,
+ &0_u32,
+ &0_u32,
+ &zero_hash(&env),
+ &None,
+ );
+ assert!(result.is_err());
+ }
+}
diff --git a/smart-contract/contracts/src/types.rs b/smart-contract/contracts/src/types.rs
index 27e643ff..8b48dbe4 100644
--- a/smart-contract/contracts/src/types.rs
+++ b/smart-contract/contracts/src/types.rs
@@ -1,5 +1,53 @@
use soroban_sdk::{contracttype, Address, BytesN, Map, String, Symbol, Val, Vec};
+// ─── Sustainability Types ──────────────────────────────────────────────────────
+
+/// Verification status of a sustainability record.
+#[contracttype]
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum SustainabilityStatus {
+ /// Submitted and awaiting third-party review.
+ Pending,
+ /// Approved and anchored on-chain with a certificate hash.
+ Verified,
+ /// Rejected due to invalid or unverifiable claims.
+ Rejected,
+}
+
+/// On-chain sustainability record for a supply-chain product.
+///
+/// Numeric environmental values use integer milli-units to avoid floating point:
+/// - `carbon_footprint_g`: CO₂ in grams
+/// - `water_usage_ml`: water consumed in millilitres
+/// - `renewable_energy_pct`: 0–100 integer percentage
+/// - `waste_recycled_pct`: 0–100 integer percentage
+#[contracttype]
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct SustainabilityRecord {
+ /// CO₂-equivalent emissions in grams (non-negative).
+ pub carbon_footprint_g: i128,
+ /// Water consumption in millilitres (non-negative).
+ pub water_usage_ml: i128,
+ /// Percentage of energy from renewable sources (0–100).
+ pub renewable_energy_pct: u32,
+ /// Percentage of waste that is recycled (0–100).
+ pub waste_recycled_pct: u32,
+ /// Hash of the off-chain labor-compliance documentation.
+ pub labor_compliance_hash: BytesN<32>,
+ /// Optional third-party certification document hash.
+ pub certificate_hash: Option>,
+ /// Current verification status.
+ pub status: SustainabilityStatus,
+ /// Ledger timestamp when this record was submitted.
+ pub recorded_at: u64,
+ /// Address that submitted this record.
+ pub recorded_by: Address,
+ /// Address that verified/rejected this record, if any.
+ pub verified_by: Option,
+ /// Ledger timestamp of verification/rejection (0 if pending).
+ pub verified_at: u64,
+}
+
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DeactInfo {
@@ -104,6 +152,7 @@ pub enum DataKey {
MultiSigConfig, // Multi-signature configuration
Proposal(u64), // Proposal by ID
NextProposalId, // Next proposal ID counter
+ Sustainability(String), // SustainabilityRecord keyed by product_id
}
#[contracttype]