diff --git a/contract/contracts/hello-world/src/autoshare_logic.rs b/contract/contracts/hello-world/src/autoshare_logic.rs index 9ffd706..951aa3c 100644 --- a/contract/contracts/hello-world/src/autoshare_logic.rs +++ b/contract/contracts/hello-world/src/autoshare_logic.rs @@ -1,7 +1,8 @@ use crate::base::errors::Error; use crate::base::events::{ - AdminTransferred, AutoshareCreated, AutoshareUpdated, ContractPaused, ContractUnpaused, - GroupActivated, GroupDeactivated, NotificationCategory, Withdrawal, + AdminTransferred, AuthorizationFailure, AutoshareCreated, AutoshareUpdated, + ContractPaused, ContractUnpaused, GroupActivated, GroupDeactivated, + NotificationCategory, Withdrawal, }; use crate::base::types::{AutoShareDetails, GroupMember, PaymentHistory}; use soroban_sdk::{contracttype, token, Address, BytesN, Env, String, Vec}; @@ -183,9 +184,12 @@ pub fn get_group_members(env: Env, id: BytesN<32>) -> Result, E pub fn add_group_member( env: Env, id: BytesN<32>, + caller: Address, address: Address, percentage: u32, ) -> Result<(), Error> { + caller.require_auth(); + // Check if contract is paused if get_paused_status(&env) { return Err(Error::ContractPaused); @@ -198,6 +202,11 @@ pub fn add_group_member( .get(&key) .ok_or(Error::NotFound)?; + if details.creator != caller { + publish_authorization_failure(&env, &caller, "add_group_member"); + return Err(Error::Unauthorized); + } + // Check if already a member for member in details.members.iter() { if member.address == address { @@ -247,6 +256,15 @@ pub fn initialize_admin(env: Env, admin: Address) { } } +fn publish_authorization_failure(env: &Env, caller: &Address, action: &str) { + AuthorizationFailure { + caller: caller.clone(), + category: NotificationCategory::Admin, + action: String::from_str(env, action), + } + .publish(env); +} + fn require_admin(env: &Env, caller: &Address) -> Result<(), Error> { let admin_key = DataKey::Admin; let admin: Address = env @@ -256,6 +274,7 @@ fn require_admin(env: &Env, caller: &Address) -> Result<(), Error> { .ok_or(Error::Unauthorized)?; if admin != *caller { + publish_authorization_failure(env, caller, "require_admin"); return Err(Error::Unauthorized); } @@ -610,6 +629,7 @@ pub fn update_members( .ok_or(Error::NotFound)?; if details.creator != caller { + publish_authorization_failure(&env, &caller, "update_members"); return Err(Error::Unauthorized); } @@ -678,6 +698,7 @@ pub fn deactivate_group(env: Env, id: BytesN<32>, caller: Address) -> Result<(), .ok_or(Error::NotFound)?; if details.creator != caller { + publish_authorization_failure(&env, &caller, "deactivate_group"); return Err(Error::Unauthorized); } @@ -713,6 +734,7 @@ pub fn activate_group(env: Env, id: BytesN<32>, caller: Address) -> Result<(), E .ok_or(Error::NotFound)?; if details.creator != caller { + publish_authorization_failure(&env, &caller, "activate_group"); return Err(Error::Unauthorized); } diff --git a/contract/contracts/hello-world/src/base/events.rs b/contract/contracts/hello-world/src/base/events.rs index a2d395c..9c97285 100644 --- a/contract/contracts/hello-world/src/base/events.rs +++ b/contract/contracts/hello-world/src/base/events.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{contractevent, contracttype, Address, BytesN}; +use soroban_sdk::{contractevent, contracttype, Address, BytesN, String}; /// High-level notification category attached to every emitted event. /// @@ -108,3 +108,14 @@ pub struct Withdrawal { pub category: NotificationCategory, pub amount: i128, } + +/// Emitted when an authorization failure is detected by the contract. +#[contractevent(data_format = "single-value")] +#[derive(Clone)] +pub struct AuthorizationFailure { + #[topic] + pub caller: Address, + #[topic] + pub category: NotificationCategory, + pub action: String, +} diff --git a/contract/contracts/hello-world/src/interfaces/autoshare.rs b/contract/contracts/hello-world/src/interfaces/autoshare.rs index af766ba..61ff816 100644 --- a/contract/contracts/hello-world/src/interfaces/autoshare.rs +++ b/contract/contracts/hello-world/src/interfaces/autoshare.rs @@ -69,7 +69,13 @@ pub trait AutoShareTrait { fn get_group_members(env: Env, id: BytesN<32>) -> Vec; /// Adds a member to a group with specified percentage. - fn add_group_member(env: Env, id: BytesN<32>, address: Address, percentage: u32); + fn add_group_member( + env: Env, + id: BytesN<32>, + caller: Address, + address: Address, + percentage: u32, + ); /// Deactivates a group. Only the creator can deactivate. fn deactivate_group(env: Env, id: BytesN<32>, caller: Address); diff --git a/contract/contracts/hello-world/src/lib.rs b/contract/contracts/hello-world/src/lib.rs index 9b4f63f..6742f3c 100644 --- a/contract/contracts/hello-world/src/lib.rs +++ b/contract/contracts/hello-world/src/lib.rs @@ -109,8 +109,14 @@ impl AutoShareContract { } /// Adds a member to a group with specified percentage. - pub fn add_group_member(env: Env, id: BytesN<32>, address: Address, percentage: u32) { - autoshare_logic::add_group_member(env, id, address, percentage).unwrap(); + pub fn add_group_member( + env: Env, + id: BytesN<32>, + caller: Address, + address: Address, + percentage: u32, + ) { + autoshare_logic::add_group_member(env, id, caller, address, percentage).unwrap(); } /// Deactivates a group. Only the creator can deactivate. diff --git a/contract/contracts/hello-world/src/tests/autoshare_test.rs b/contract/contracts/hello-world/src/tests/autoshare_test.rs index 9b2b90e..65ad43c 100644 --- a/contract/contracts/hello-world/src/tests/autoshare_test.rs +++ b/contract/contracts/hello-world/src/tests/autoshare_test.rs @@ -602,6 +602,29 @@ fn test_add_group_member_success() { assert_eq!(final_members.get(2).unwrap().percentage, 34); } +#[test] +#[should_panic] // Unauthorized +fn test_add_group_member_unauthorized() { + let test_env = setup_test_env(); + let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); + + let creator = test_env.users.get(0).unwrap().clone(); + let id = BytesN::from_array(&test_env.env, &[1u8; 32]); + let name = String::from_str(&test_env.env, "Unauthorized Add Test"); + + let mut members = Vec::new(&test_env.env); + members.push_back(GroupMember { + address: Address::generate(&test_env.env), + percentage: 100, + }); + + create_helper(&client, &id, &name, &creator, &members, &test_env); + + let other_user = Address::generate(&test_env.env); + let member_to_add = Address::generate(&test_env.env); + client.add_group_member(&id, &other_user, &member_to_add, &50); +} + #[test] #[should_panic] // AlreadyExists fn test_add_duplicate_member() { @@ -622,7 +645,7 @@ fn test_add_duplicate_member() { create_helper(&client, &id, &name, &creator, &members, &test_env); // Try to add the same member again - should fail - client.add_group_member(&id, &member1, &50); + client.add_group_member(&id, &creator, &member1, &50); } #[test] @@ -631,10 +654,11 @@ fn test_add_member_to_non_existent_group() { let test_env = setup_test_env(); let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); + let creator = test_env.users.get(0).unwrap().clone(); let id = BytesN::from_array(&test_env.env, &[99u8; 32]); let member = Address::generate(&test_env.env); - client.add_group_member(&id, &member, &50); + client.add_group_member(&id, &creator, &member, &50); } #[test] @@ -659,7 +683,7 @@ fn test_add_member_invalid_total_percentage() { // Try to add another member with 50% (total would be 150%) - should fail let member2 = Address::generate(&test_env.env); - client.add_group_member(&id, &member2, &50); + client.add_group_member(&id, &creator, &member2, &50); } #[test] diff --git a/contract/contracts/hello-world/src/tests/pause_test.rs b/contract/contracts/hello-world/src/tests/pause_test.rs index 5005b07..60d6c94 100644 --- a/contract/contracts/hello-world/src/tests/pause_test.rs +++ b/contract/contracts/hello-world/src/tests/pause_test.rs @@ -181,7 +181,7 @@ fn test_add_member_fails_when_paused() { token_admin_client.mint(&creator, &10000000); client.create(&id, &name, &creator, &100u32, &token_address); client.pause(&admin); - client.add_group_member(&id, &member, &50u32); + client.add_group_member(&id, &creator, &member, &50u32); } #[test]