diff --git a/dongle-smartcontract/Cargo.toml b/dongle-smartcontract/Cargo.toml
index addc8f4..ebffde5 100644
--- a/dongle-smartcontract/Cargo.toml
+++ b/dongle-smartcontract/Cargo.toml
@@ -7,10 +7,10 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
-soroban-sdk = "21.7.6"
+soroban-sdk = { version = "22.0.0" }
[dev-dependencies]
-soroban-sdk = { version = "21.7.6", features = ["testutils"] }
+soroban-sdk = { version = "22.0.0", features = ["testutils"] }
[features]
testutils = ["soroban-sdk/testutils"]
@@ -23,6 +23,7 @@ strip = "symbols"
debug-assertions = false
panic = "abort"
codegen-units = 1
+lto = true
[profile.release-with-logs]
inherits = "release"
diff --git a/dongle-smartcontract/src/admin_manager.rs b/dongle-smartcontract/src/admin_manager.rs
new file mode 100644
index 0000000..9d4cf5f
--- /dev/null
+++ b/dongle-smartcontract/src/admin_manager.rs
@@ -0,0 +1,252 @@
+//! Admin role management and access control
+//!
+//! This module provides functionality for managing admin roles and enforcing
+//! access control across privileged contract operations.
+
+use crate::errors::ContractError;
+use crate::events::{publish_admin_added_event, publish_admin_removed_event};
+use crate::storage_keys::StorageKey;
+use soroban_sdk::{Address, Env, Vec};
+
+pub struct AdminManager;
+
+impl AdminManager {
+ /// Initialize the contract with the first admin
+ pub fn initialize(env: &Env, admin: Address) {
+ // Don't require auth during initialization - this is typically called once during contract deployment
+
+ // Set the admin in storage
+ env.storage()
+ .persistent()
+ .set(&StorageKey::Admin(admin.clone()), &true);
+
+ // Initialize admin list
+ let mut admins = Vec::new(env);
+ admins.push_back(admin.clone());
+ env.storage()
+ .persistent()
+ .set(&StorageKey::AdminList, &admins);
+
+ publish_admin_added_event(env, admin);
+ }
+
+ /// Add a new admin (only callable by existing admins)
+ pub fn add_admin(env: &Env, caller: Address, new_admin: Address) -> Result<(), ContractError> {
+ caller.require_auth();
+
+ // Verify caller is an admin
+ Self::require_admin(env, &caller)?;
+
+ // Check if already an admin
+ if Self::is_admin(env, &new_admin) {
+ return Ok(()); // Already an admin, no-op
+ }
+
+ // Add to admin mapping
+ env.storage()
+ .persistent()
+ .set(&StorageKey::Admin(new_admin.clone()), &true);
+
+ // Add to admin list
+ let mut admins = Self::get_admin_list(env);
+ admins.push_back(new_admin.clone());
+ env.storage()
+ .persistent()
+ .set(&StorageKey::AdminList, &admins);
+
+ publish_admin_added_event(env, new_admin);
+
+ Ok(())
+ }
+
+ /// Remove an admin (only callable by existing admins)
+ pub fn remove_admin(
+ env: &Env,
+ caller: Address,
+ admin_to_remove: Address,
+ ) -> Result<(), ContractError> {
+ caller.require_auth();
+
+ // Verify caller is an admin
+ Self::require_admin(env, &caller)?;
+
+ // Prevent removing the last admin
+ let admins = Self::get_admin_list(env);
+ if admins.len() <= 1 {
+ return Err(ContractError::CannotRemoveLastAdmin);
+ }
+
+ // Check if the address is actually an admin
+ if !Self::is_admin(env, &admin_to_remove) {
+ return Err(ContractError::AdminNotFound);
+ }
+
+ // Remove from admin mapping
+ env.storage()
+ .persistent()
+ .remove(&StorageKey::Admin(admin_to_remove.clone()));
+
+ // Remove from admin list
+ let mut new_admins = Vec::new(env);
+ for admin in admins.iter() {
+ if admin != admin_to_remove {
+ new_admins.push_back(admin);
+ }
+ }
+ env.storage()
+ .persistent()
+ .set(&StorageKey::AdminList, &new_admins);
+
+ publish_admin_removed_event(env, admin_to_remove);
+
+ Ok(())
+ }
+
+ /// Check if an address is an admin
+ pub fn is_admin(env: &Env, address: &Address) -> bool {
+ env.storage()
+ .persistent()
+ .get(&StorageKey::Admin(address.clone()))
+ .unwrap_or(false)
+ }
+
+ /// Require that the caller is an admin, otherwise return an error
+ pub fn require_admin(env: &Env, address: &Address) -> Result<(), ContractError> {
+ if Self::is_admin(env, address) {
+ Ok(())
+ } else {
+ Err(ContractError::AdminOnly)
+ }
+ }
+
+ /// Get the list of all admins
+ pub fn get_admin_list(env: &Env) -> Vec
{
+ env.storage()
+ .persistent()
+ .get(&StorageKey::AdminList)
+ .unwrap_or(Vec::new(env))
+ }
+
+ /// Get the count of admins
+ pub fn get_admin_count(env: &Env) -> u32 {
+ Self::get_admin_list(env).len()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::DongleContract;
+ use crate::DongleContractClient;
+ use soroban_sdk::{testutils::Address as _, Address, Env};
+
+ #[test]
+ fn test_initialize_admin() {
+ let env = Env::default();
+ let contract_id = env.register_contract(None, DongleContract);
+ let client = DongleContractClient::new(&env, &contract_id);
+ let admin = Address::generate(&env);
+
+ client.mock_all_auths().initialize(&admin);
+
+ assert!(client.is_admin(&admin));
+ assert_eq!(client.get_admin_count(), 1);
+ }
+
+ #[test]
+ fn test_add_admin() {
+ let env = Env::default();
+ let contract_id = env.register_contract(None, DongleContract);
+ let client = DongleContractClient::new(&env, &contract_id);
+ let admin1 = Address::generate(&env);
+ let admin2 = Address::generate(&env);
+
+ client.mock_all_auths().initialize(&admin1);
+ client.mock_all_auths().add_admin(&admin1, &admin2);
+
+ assert!(client.is_admin(&admin2));
+ assert_eq!(client.get_admin_count(), 2);
+ }
+
+ #[test]
+ fn test_add_admin_unauthorized() {
+ let env = Env::default();
+ let contract_id = env.register_contract(None, DongleContract);
+ let client = DongleContractClient::new(&env, &contract_id);
+ let admin = Address::generate(&env);
+ let non_admin = Address::generate(&env);
+ let new_admin = Address::generate(&env);
+
+ client.mock_all_auths().initialize(&admin);
+ let result = client.mock_all_auths().try_add_admin(&non_admin, &new_admin);
+
+ assert_eq!(result, Err(Ok(crate::errors::ContractError::AdminOnly)));
+ assert!(!client.is_admin(&new_admin));
+ }
+
+ #[test]
+ fn test_remove_admin() {
+ let env = Env::default();
+ let contract_id = env.register_contract(None, DongleContract);
+ let client = DongleContractClient::new(&env, &contract_id);
+ let admin1 = Address::generate(&env);
+ let admin2 = Address::generate(&env);
+
+ client.mock_all_auths().initialize(&admin1);
+ client.mock_all_auths().add_admin(&admin1, &admin2);
+ client.mock_all_auths().remove_admin(&admin1, &admin2);
+
+ assert!(!client.is_admin(&admin2));
+ assert_eq!(client.get_admin_count(), 1);
+ }
+
+ #[test]
+ fn test_cannot_remove_last_admin() {
+ let env = Env::default();
+ let contract_id = env.register_contract(None, DongleContract);
+ let client = DongleContractClient::new(&env, &contract_id);
+ let admin = Address::generate(&env);
+
+ client.mock_all_auths().initialize(&admin);
+ let result = client.mock_all_auths().try_remove_admin(&admin, &admin);
+
+ assert_eq!(result, Err(Ok(crate::errors::ContractError::CannotRemoveLastAdmin)));
+ assert!(client.is_admin(&admin));
+ }
+
+ #[test]
+ fn test_remove_non_existent_admin() {
+ let env = Env::default();
+ let contract_id = env.register_contract(None, DongleContract);
+ let client = DongleContractClient::new(&env, &contract_id);
+ let admin = Address::generate(&env);
+ let non_admin = Address::generate(&env);
+ let another_admin = Address::generate(&env);
+
+ client.mock_all_auths().initialize(&admin);
+ client.mock_all_auths().add_admin(&admin, &another_admin);
+ let result = client.mock_all_auths().try_remove_admin(&admin, &non_admin);
+
+ assert_eq!(result, Err(Ok(crate::errors::ContractError::AdminNotFound)));
+ assert!(client.is_admin(&another_admin));
+ }
+
+ #[test]
+ fn test_get_admin_list() {
+ let env = Env::default();
+ let contract_id = env.register_contract(None, DongleContract);
+ let client = DongleContractClient::new(&env, &contract_id);
+ let admin1 = Address::generate(&env);
+ let admin2 = Address::generate(&env);
+ let admin3 = Address::generate(&env);
+
+ client.mock_all_auths().initialize(&admin1);
+ client.mock_all_auths().add_admin(&admin1, &admin2);
+ client.mock_all_auths().add_admin(&admin1, &admin3);
+
+ let admins = client.get_admin_list();
+ assert_eq!(admins.len(), 3);
+ assert!(admins.contains(&admin1));
+ assert!(admins.contains(&admin2));
+ assert!(admins.contains(&admin3));
+ }
+}
diff --git a/dongle-smartcontract/src/constants.rs b/dongle-smartcontract/src/constants.rs
index a3e77ce..0abc2a8 100644
--- a/dongle-smartcontract/src/constants.rs
+++ b/dongle-smartcontract/src/constants.rs
@@ -1,26 +1,35 @@
+#![allow(dead_code)]
//! Contract limits and validation constants. Kept in one place for easy future updates.
/// Maximum number of projects a single user (address) can register. Prevents abuse.
+#[allow(dead_code)]
pub const MAX_PROJECTS_PER_USER: u32 = 50;
/// Minimum length for name, description, category (must be non-empty after trim in validation).
+#[allow(dead_code)]
pub const MIN_STRING_LEN: usize = 1;
/// Maximum length for project name.
-pub const MAX_NAME_LEN: usize = 128;
+pub const MAX_NAME_LEN: usize = 50;
/// Maximum length for project description.
+#[allow(dead_code)]
pub const MAX_DESCRIPTION_LEN: usize = 2048;
/// Maximum length for category.
+#[allow(dead_code)]
pub const MAX_CATEGORY_LEN: usize = 64;
/// Maximum length for website URL.
+#[allow(dead_code)]
pub const MAX_WEBSITE_LEN: usize = 256;
/// Maximum length for any CID (logo, metadata, comment, evidence).
+#[allow(dead_code)]
pub const MAX_CID_LEN: usize = 128;
/// Valid rating range (inclusive). Reviews must be in [RATING_MIN, RATING_MAX]. u32 for Soroban Val.
+#[allow(dead_code)]
pub const RATING_MIN: u32 = 1;
+#[allow(dead_code)]
pub const RATING_MAX: u32 = 5;
diff --git a/dongle-smartcontract/src/errors.rs b/dongle-smartcontract/src/errors.rs
index b7621c4..25fe80a 100644
--- a/dongle-smartcontract/src/errors.rs
+++ b/dongle-smartcontract/src/errors.rs
@@ -15,34 +15,45 @@ pub enum ContractError {
InvalidRating = 4,
/// Review not found
ReviewNotFound = 5,
+ /// User has already reviewed this project
+ AlreadyReviewed = 6,
/// Verification record not found
- VerificationNotFound = 6,
+ VerificationNotFound = 7,
/// Invalid verification status transition
- InvalidStatusTransition = 7,
+ InvalidStatusTransition = 8,
/// Only admin can perform this action
- AdminOnly = 8,
+ AdminOnly = 9,
/// Invalid fee amount
- InvalidFeeAmount = 9,
+ InvalidFeeAmount = 10,
/// Insufficient fee paid
- InsufficientFee = 10,
+ InsufficientFee = 11,
/// Invalid project data - missing required fields
- InvalidProjectData = 11,
+ InvalidProjectData = 12,
/// Project name too long
- ProjectNameTooLong = 12,
+ ProjectNameTooLong = 13,
/// Project description too long
- ProjectDescriptionTooLong = 13,
+ ProjectDescriptionTooLong = 14,
/// Invalid project category
- InvalidProjectCategory = 14,
+ InvalidProjectCategory = 15,
/// Verification already processed
- VerificationAlreadyProcessed = 15,
+ VerificationAlreadyProcessed = 16,
/// Cannot review own project
- CannotReviewOwnProject = 16,
+ CannotReviewOwnProject = 17,
/// Fee configuration not set
- FeeConfigNotSet = 17,
+ FeeConfigNotSet = 18,
/// Treasury address not set
- TreasuryNotSet = 18,
- /// User has already reviewed this project
- AlreadyReviewed = 19, // I added your error here with a new unique ID
- /// Review is already deleted
+ TreasuryNotSet = 19,
+ /// Review already deleted
ReviewAlreadyDeleted = 20,
+ /// Invalid project name format (contains invalid characters)
+ InvalidProjectNameFormat = 21,
+ /// Not the reviewer (trying to modify another user's review)
+ NotReviewer = 22,
+ /// Cannot remove the last admin
+ CannotRemoveLastAdmin = 23,
+ /// Admin not found
+ AdminNotFound = 24,
}
+
+// Legacy alias to avoid breaking any code that uses `Error` directly
+pub type Error = ContractError;
\ No newline at end of file
diff --git a/dongle-smartcontract/src/events.rs b/dongle-smartcontract/src/events.rs
index 9949c10..d35b6ff 100644
--- a/dongle-smartcontract/src/events.rs
+++ b/dongle-smartcontract/src/events.rs
@@ -28,6 +28,7 @@ pub fn publish_review_event(
.publish((REVIEW, action_sym, project_id, reviewer), event_data);
}
+#[allow(dead_code)]
pub fn publish_fee_paid_event(env: &Env, project_id: u64, amount: u128) {
env.events().publish(
(symbol_short!("FEE"), symbol_short!("PAID"), project_id),
@@ -41,24 +42,39 @@ pub fn publish_fee_set_event(env: &Env, verification_fee: u128, registration_fee
(verification_fee, registration_fee),
);
}
-
-pub fn publish_verification_requested_event(env: &Env, project_id: u64, requester: Address) {
- env.events().publish(
+#[allow(dead_code)]
+pub fn publish_verification_requested_event(
+ env: &Env,
+ project_id: u64,
+ requester: Address,
+ evidence_cid: String,
+) {
+ env.events().publish(
(symbol_short!("VERIFY"), symbol_short!("REQ"), project_id),
- requester,
+ (requester, evidence_cid),
);
}
-pub fn publish_verification_approved_event(env: &Env, project_id: u64) {
+pub fn publish_verification_approved_event(env: &Env, project_id: u64, admin: Address) {
env.events().publish(
(symbol_short!("VERIFY"), symbol_short!("APP"), project_id),
- project_id,
+ admin,
);
}
-pub fn publish_verification_rejected_event(env: &Env, project_id: u64) {
+pub fn publish_verification_rejected_event(env: &Env, project_id: u64, admin: Address) {
env.events().publish(
(symbol_short!("VERIFY"), symbol_short!("REJ"), project_id),
- project_id,
+ admin,
);
}
+
+pub fn publish_admin_added_event(env: &Env, admin: Address) {
+ env.events()
+ .publish((symbol_short!("ADMIN"), symbol_short!("ADDED")), admin);
+}
+
+pub fn publish_admin_removed_event(env: &Env, admin: Address) {
+ env.events()
+ .publish((symbol_short!("ADMIN"), symbol_short!("REMOVED")), admin);
+}
diff --git a/dongle-smartcontract/src/fee_manager.rs b/dongle-smartcontract/src/fee_manager.rs
index fa15986..eaefe20 100644
--- a/dongle-smartcontract/src/fee_manager.rs
+++ b/dongle-smartcontract/src/fee_manager.rs
@@ -1,8 +1,10 @@
//! Fee configuration and payment with validation and events.
+use crate::admin_manager::AdminManager;
use crate::errors::ContractError;
-use crate::events::publish_fee_set_event;
-use crate::types::{DataKey, FeeConfig};
+use crate::events::{publish_fee_paid_event, publish_fee_set_event};
+use crate::storage_keys::StorageKey;
+use crate::types::FeeConfig;
use soroban_sdk::{Address, Env};
pub struct FeeManager;
@@ -10,80 +12,95 @@ pub struct FeeManager;
impl FeeManager {
pub fn set_fee(
env: &Env,
- _admin: Address,
+ admin: Address,
token: Option,
amount: u128,
treasury: Address,
) -> Result<(), ContractError> {
+ admin.require_auth();
+ AdminManager::require_admin(env, &admin)?;
+
let config = FeeConfig {
token,
verification_fee: amount,
registration_fee: 0,
};
- env.storage().persistent().set(&DataKey::FeeConfig, &config);
env.storage()
.persistent()
- .set(&DataKey::Treasury, &treasury);
+ .set(&StorageKey::FeeConfig, &config);
+ env.storage()
+ .persistent()
+ .set(&StorageKey::Treasury, &treasury);
publish_fee_set_event(env, amount, 0);
Ok(())
}
pub fn pay_fee(
- _env: &Env,
- _payer: Address,
- _project_id: u64,
- _token: Option,
+ env: &Env,
+ payer: Address,
+ project_id: u64,
+ token: Option,
) -> Result<(), ContractError> {
- todo!("Fee payment logic not implemented")
- }
+ payer.require_auth();
+
+ let config = Self::get_fee_config(env)?;
+
+ if config.token != token {
+ return Err(ContractError::InvalidProjectData);
+ }
+
+ let amount = config.verification_fee;
+ if amount > 0 {
+ if let Some(token_address) = config.token {
+ let treasury: Address = env
+ .storage()
+ .persistent()
+ .get(&StorageKey::Treasury)
+ .ok_or(ContractError::TreasuryNotSet)?;
+ let client = soroban_sdk::token::Client::new(env, &token_address);
+ client.transfer(&payer, &treasury, &(amount as i128));
+ } else {
+ return Err(ContractError::FeeConfigNotSet);
+ }
+ }
- pub fn get_fee_config(env: &Env) -> Result {
env.storage()
.persistent()
- .get(&DataKey::FeeConfig)
- .ok_or(ContractError::FeeConfigNotSet)
- }
+ .set(&StorageKey::FeePaidForProject(project_id), &true);
- #[allow(dead_code)]
- pub fn set_treasury(
- _env: &Env,
- _admin: Address,
- _treasury: Address,
- ) -> Result<(), ContractError> {
- todo!("Treasury setting logic not implemented")
- }
+ publish_fee_paid_event(env, project_id, amount);
- #[allow(dead_code)]
- pub fn get_treasury(_env: &Env) -> Result {
- todo!("Treasury address retrieval logic not implemented")
+ Ok(())
}
- #[allow(dead_code)]
- pub fn get_operation_fee(_env: &Env, operation_type: &str) -> Result {
- match operation_type {
- "verification" => Ok(1000000),
- "registration" => Ok(0),
- _ => Err(ContractError::InvalidProjectData),
- }
+ pub fn get_fee_config(env: &Env) -> Result {
+ env.storage()
+ .persistent()
+ .get(&StorageKey::FeeConfig)
+ .ok_or(ContractError::FeeConfigNotSet)
}
- #[allow(dead_code)]
- pub fn fee_config_exists(_env: &Env) -> bool {
- false
+ pub fn is_fee_paid(env: &Env, project_id: u64) -> bool {
+ env.storage()
+ .persistent()
+ .get(&StorageKey::FeePaidForProject(project_id))
+ .unwrap_or(false)
}
- #[allow(dead_code)]
- pub fn treasury_exists(_env: &Env) -> bool {
- false
+ pub fn get_treasury(env: &Env) -> Result {
+ env.storage()
+ .persistent()
+ .get(&StorageKey::Treasury)
+ .ok_or(ContractError::TreasuryNotSet)
}
- #[allow(dead_code)]
- pub fn refund_fee(
- _env: &Env,
- _recipient: Address,
- _amount: u128,
- _token: Option,
- ) -> Result<(), ContractError> {
- todo!("Fee refund logic not implemented")
+ pub fn set_treasury(env: &Env, admin: Address, treasury: Address) -> Result<(), ContractError> {
+ admin.require_auth();
+ AdminManager::require_admin(env, &admin)?;
+
+ env.storage()
+ .persistent()
+ .set(&StorageKey::Treasury, &treasury);
+ Ok(())
}
}
diff --git a/dongle-smartcontract/src/lib.rs b/dongle-smartcontract/src/lib.rs
index ff8d6cc..249b2fc 100644
--- a/dongle-smartcontract/src/lib.rs
+++ b/dongle-smartcontract/src/lib.rs
@@ -1,5 +1,12 @@
#![no_std]
+//! # Dongle Smart Contract
+//!
+//! A decentralized project registry and discovery platform built on Stellar/Soroban.
+//! This contract enables transparent project registration, community reviews, and
+//! verification processes for the Stellar ecosystem.
+
+mod admin_manager;
mod constants;
mod errors;
mod events;
@@ -15,10 +22,14 @@ mod verification_registry;
#[cfg(test)]
mod test;
+use crate::admin_manager::AdminManager;
+use crate::errors::ContractError;
use crate::fee_manager::FeeManager;
use crate::project_registry::ProjectRegistry;
use crate::review_registry::ReviewRegistry;
-use crate::types::{FeeConfig, Project, Review, VerificationRecord};
+use crate::types::{
+ FeeConfig, Project, ProjectRegistrationParams, ProjectUpdateParams, Review, VerificationRecord,
+};
use crate::verification_registry::VerificationRegistry;
use soroban_sdk::{contract, contractimpl, Address, Env, String, Vec};
@@ -27,56 +38,92 @@ pub struct DongleContract;
#[contractimpl]
impl DongleContract {
- // --- Project Registry ---
+
+ // ==========================================
+ // INITIALIZATION & ADMIN FUNCTIONS
+ // ==========================================
+
+ pub fn initialize(env: Env, admin: Address) {
+ AdminManager::initialize(&env, admin);
+ }
+
+ pub fn add_admin(env: Env, caller: Address, new_admin: Address) -> Result<(), ContractError> {
+ AdminManager::add_admin(&env, caller, new_admin)
+ }
+
+ pub fn remove_admin(
+ env: Env,
+ caller: Address,
+ admin_to_remove: Address,
+ ) -> Result<(), ContractError> {
+ AdminManager::remove_admin(&env, caller, admin_to_remove)
+ }
+
+ pub fn is_admin(env: Env, address: Address) -> bool {
+ AdminManager::is_admin(&env, &address)
+ }
+
+ pub fn get_admin_list(env: Env) -> Vec {
+ AdminManager::get_admin_list(&env)
+ }
+
+ pub fn get_admin_count(env: Env) -> u32 {
+ AdminManager::get_admin_count(&env)
+ }
+
+ // ==========================================
+ // FEE MANAGER FUNCTIONS
+ // ==========================================
+
+ pub fn set_fee(
+ env: Env,
+ admin: Address,
+ token: Option,
+ amount: u128,
+ treasury: Address,
+ ) -> Result<(), ContractError> {
+ FeeManager::set_fee(&env, admin, token, amount, treasury)
+ }
+
+ pub fn pay_fee(
+ env: Env,
+ payer: Address,
+ project_id: u64,
+ token: Option,
+ ) -> Result<(), ContractError> {
+ FeeManager::pay_fee(&env, payer, project_id, token)
+ }
+
+ pub fn get_fee_config(env: Env) -> Result {
+ FeeManager::get_fee_config(&env)
+ }
+
+ // ==========================================
+ // PROJECT REGISTRY FUNCTIONS
+ // ==========================================
pub fn register_project(
env: Env,
- owner: Address,
- name: String,
- description: String,
- category: String,
- website: Option,
- logo_cid: Option,
- metadata_cid: Option,
- ) -> u64 {
- ProjectRegistry::register_project(
- &env,
- owner,
- name,
- description,
- category,
- website,
- logo_cid,
- metadata_cid,
- )
+ params: ProjectRegistrationParams,
+ ) -> Result {
+ ProjectRegistry::register_project(&env, params)
}
pub fn update_project(
env: Env,
- project_id: u64,
- caller: Address,
- name: Option,
- description: Option,
- category: Option,
- website: Option