diff --git a/contracts/royalty-splitter/src/contract.rs b/contracts/royalty-splitter/src/contract.rs
index 42ee77d..643f9f6 100644
--- a/contracts/royalty-splitter/src/contract.rs
+++ b/contracts/royalty-splitter/src/contract.rs
@@ -4,9 +4,9 @@ use soroban_sdk::{
use crate::{
storage::{
- is_initialized, load_beneficiaries, load_shares, load_token, save_beneficiaries,
- save_shares, save_token, set_initialized, LEDGER_TTL_BUMP, LEDGER_TTL_THRESHOLD,
- MAX_BENEFICIARIES,
+ is_initialized, load_admin, load_beneficiaries, load_shares, load_token,
+ save_admin, save_beneficiaries, save_shares, save_token, set_initialized,
+ LEDGER_TTL_BUMP, LEDGER_TTL_THRESHOLD, MAX_BENEFICIARIES,
},
types::SplitterError,
};
@@ -16,9 +16,15 @@ pub struct RoyaltySplitter;
#[contractimpl]
impl RoyaltySplitter {
- /// Lock in the token, beneficiaries, and BPS shares forever.
+ /// Lock in the admin, token, beneficiaries, and BPS shares forever.
/// Shares must sum to exactly 10 000. Can only be called once.
- pub fn initialize(env: Env, token: Address, beneficiaries: Vec
, shares: Vec) {
+ pub fn initialize(
+ env: Env,
+ admin: Address,
+ token: Address,
+ beneficiaries: Vec,
+ shares: Vec,
+ ) {
if is_initialized(&env) {
panic_with_error!(&env, SplitterError::AlreadyInitialized);
}
@@ -40,6 +46,7 @@ impl RoyaltySplitter {
panic_with_error!(&env, SplitterError::InvalidShares);
}
+ save_admin(&env, &admin);
save_token(&env, &token);
save_beneficiaries(&env, &beneficiaries);
save_shares(&env, &shares);
@@ -88,6 +95,55 @@ impl RoyaltySplitter {
}
}
+ /// Update the royalty split configuration. Can only be called by the admin.
+ /// Shares must sum to exactly 10 000.
+ pub fn update_royalty_split(
+ env: Env,
+ admin: Address,
+ beneficiaries: Vec,
+ shares: Vec,
+ ) {
+ // Require admin authentication
+ admin.require_auth();
+
+ if !is_initialized(&env) {
+ panic_with_error!(&env, SplitterError::NotInitialized);
+ }
+
+ // Verify the provided admin matches the stored admin
+ let stored_admin = load_admin(&env);
+ if admin != stored_admin {
+ panic_with_error!(&env, SplitterError::Unauthorized);
+ }
+
+ // Validate inputs
+ if beneficiaries.is_empty() {
+ panic_with_error!(&env, SplitterError::NoBeneficiaries);
+ }
+ if beneficiaries.len() > MAX_BENEFICIARIES {
+ panic_with_error!(&env, SplitterError::TooManyBeneficiaries);
+ }
+ if beneficiaries.len() != shares.len() {
+ panic_with_error!(&env, SplitterError::LengthMismatch);
+ }
+
+ let mut total: u32 = 0;
+ for share in shares.iter() {
+ total += share;
+ }
+ if total != 10_000 {
+ panic_with_error!(&env, SplitterError::InvalidShares);
+ }
+
+ // Update storage
+ save_beneficiaries(&env, &beneficiaries);
+ save_shares(&env, &shares);
+
+ env.storage()
+ .instance()
+ .extend_ttl(LEDGER_TTL_THRESHOLD, LEDGER_TTL_BUMP);
+ }
+
pub fn get_token(env: Env) -> Address {
load_token(&env)
}
diff --git a/contracts/royalty-splitter/src/storage.rs b/contracts/royalty-splitter/src/storage.rs
index bff8492..66b1785 100644
--- a/contracts/royalty-splitter/src/storage.rs
+++ b/contracts/royalty-splitter/src/storage.rs
@@ -4,6 +4,9 @@ pub const MAX_BENEFICIARIES: u32 = 20;
pub const LEDGER_TTL_BUMP: u32 = 432_000;
pub const LEDGER_TTL_THRESHOLD: u32 = 144_000;
+// Add these to the storage module
+const ADMIN_KEY: &[u8] = b"admin";
+
#[contracttype]
pub enum DataKey {
Initialized,
@@ -73,3 +76,16 @@ pub fn load_shares(env: &Env) -> Vec {
.get::>(&DataKey::Shares)
.expect("shares not set")
}
+
+
+pub fn save_admin(env: &Env, admin: &Address) {
+ env.storage().instance().set(&ADMIN_KEY, admin);
+}
+
+pub fn load_admin(env: &Env) -> Address {
+ env.storage().instance().get(&ADMIN_KEY).unwrap()
+}
+
+pub fn has_admin(env: &Env) -> bool {
+ env.storage().instance().has(&ADMIN_KEY)
+}
diff --git a/contracts/royalty-splitter/src/test.rs b/contracts/royalty-splitter/src/test.rs
index 7cc297e..1e9d93c 100644
--- a/contracts/royalty-splitter/src/test.rs
+++ b/contracts/royalty-splitter/src/test.rs
@@ -13,6 +13,7 @@ fn setup() -> (
RoyaltySplitterClient<'static>,
Address, // token
Address, // contract_id
+ Address, // admin
) {
let env = Env::default();
env.mock_all_auths();
@@ -20,23 +21,25 @@ fn setup() -> (
let contract_id = env.register(RoyaltySplitter, ());
let client = RoyaltySplitterClient::new(&env, &contract_id);
+ let admin = Address::generate(&env);
let token_admin = Address::generate(&env);
let token = env
.register_stellar_asset_contract_v2(token_admin)
.address();
- (env, client, token, contract_id)
+ (env, client, token, contract_id, admin)
}
// ── initialize ────────────────────────────────────────────────
#[test]
fn test_initialize_stores_config() {
- let (env, client, token, _) = setup();
+ let (env, client, token, _, admin) = setup();
let alice = Address::generate(&env);
let bob = Address::generate(&env);
client.initialize(
+ &admin,
&token,
&vec![&env, alice.clone(), bob.clone()],
&vec![&env, 6_000_u32, 4_000_u32],
@@ -53,11 +56,12 @@ fn test_initialize_stores_config() {
#[test]
fn test_double_initialize_is_rejected() {
- let (env, client, token, _) = setup();
+ let (env, client, token, _, admin) = setup();
let alice = Address::generate(&env);
let bob = Address::generate(&env);
client.initialize(
+ &admin,
&token,
&vec![&env, alice.clone(), bob.clone()],
&vec![&env, 5_000_u32, 5_000_u32],
@@ -65,6 +69,7 @@ fn test_double_initialize_is_rejected() {
let err = client
.try_initialize(
+ &admin,
&token,
&vec![&env, alice, bob],
&vec![&env, 5_000_u32, 5_000_u32],
@@ -77,12 +82,13 @@ fn test_double_initialize_is_rejected() {
#[test]
fn test_shares_not_summing_to_10000_is_rejected() {
- let (env, client, token, _) = setup();
+ let (env, client, token, _, admin) = setup();
let alice = Address::generate(&env);
let bob = Address::generate(&env);
let err = client
.try_initialize(
+ &admin,
&token,
&vec![&env, alice, bob],
&vec![&env, 5_000_u32, 4_000_u32], // sums to 9000
@@ -95,11 +101,12 @@ fn test_shares_not_summing_to_10000_is_rejected() {
#[test]
fn test_length_mismatch_is_rejected() {
- let (env, client, token, _) = setup();
+ let (env, client, token, _, admin) = setup();
let alice = Address::generate(&env);
let err = client
.try_initialize(
+ &admin,
&token,
&vec![&env, alice],
&vec![&env, 5_000_u32, 5_000_u32],
@@ -112,10 +119,10 @@ fn test_length_mismatch_is_rejected() {
#[test]
fn test_empty_beneficiaries_is_rejected() {
- let (env, client, token, _) = setup();
+ let (env, client, token, _, admin) = setup();
let err = client
- .try_initialize(&token, &vec![&env], &vec![&env])
+ .try_initialize(&admin, &token, &vec![&env], &vec![&env])
.unwrap_err()
.unwrap();
@@ -126,12 +133,13 @@ fn test_empty_beneficiaries_is_rejected() {
#[test]
fn test_distribute_two_parties() {
- let (env, client, token, contract_id) = setup();
+ let (env, client, token, contract_id, admin) = setup();
let alice = Address::generate(&env);
let bob = Address::generate(&env);
let caller = Address::generate(&env);
client.initialize(
+ &admin,
&token,
&vec![&env, alice.clone(), bob.clone()],
&vec![&env, 6_000_u32, 4_000_u32],
@@ -150,13 +158,14 @@ fn test_distribute_two_parties() {
#[test]
fn test_distribute_three_parties() {
- let (env, client, token, contract_id) = setup();
+ let (env, client, token, contract_id, admin) = setup();
let a = Address::generate(&env);
let b = Address::generate(&env);
let c = Address::generate(&env);
let caller = Address::generate(&env);
client.initialize(
+ &admin,
&token,
&vec![&env, a.clone(), b.clone(), c.clone()],
&vec![&env, 3_334_u32, 3_333_u32, 3_333_u32],
@@ -178,13 +187,14 @@ fn test_distribute_three_parties() {
#[test]
fn test_distribute_rounding_no_dust_trapped() {
- let (env, client, token, contract_id) = setup();
+ let (env, client, token, contract_id, admin) = setup();
let alice = Address::generate(&env);
let bob = Address::generate(&env);
let caller = Address::generate(&env);
// 3333 + 6667 = 10000; with balance=10 alice gets floor(3.333)=3, bob gets 6, caller gets 1
client.initialize(
+ &admin,
&token,
&vec![&env, alice.clone(), bob.clone()],
&vec![&env, 3_333_u32, 6_667_u32],
@@ -205,12 +215,13 @@ fn test_distribute_rounding_no_dust_trapped() {
#[test]
fn test_distribute_empty_balance_is_noop() {
- let (env, client, token, contract_id) = setup();
+ let (env, client, token, contract_id, admin) = setup();
let alice = Address::generate(&env);
let bob = Address::generate(&env);
let caller = Address::generate(&env);
client.initialize(
+ &admin,
&token,
&vec![&env, alice.clone(), bob.clone()],
&vec![&env, 5_000_u32, 5_000_u32],
@@ -227,12 +238,13 @@ fn test_distribute_empty_balance_is_noop() {
#[test]
fn test_distribute_callable_by_anyone() {
- let (env, client, token, contract_id) = setup();
+ let (env, client, token, contract_id, admin) = setup();
let alice = Address::generate(&env);
let bob = Address::generate(&env);
let caller = Address::generate(&env);
client.initialize(
+ &admin,
&token,
&vec![&env, alice.clone(), bob.clone()],
&vec![&env, 7_000_u32, 3_000_u32],
@@ -252,7 +264,7 @@ fn test_distribute_callable_by_anyone() {
#[test]
fn test_distribute_before_initialize_is_rejected() {
- let (env, client, token, _) = setup();
+ let (env, client, token, _, _) = setup();
let caller = Address::generate(&env);
let err = client.try_distribute(&token, &caller).unwrap_err().unwrap();
@@ -261,11 +273,16 @@ fn test_distribute_before_initialize_is_rejected() {
#[test]
fn test_distribute_single_beneficiary_gets_all() {
- let (env, client, token, contract_id) = setup();
+ let (env, client, token, contract_id, admin) = setup();
let alice = Address::generate(&env);
let caller = Address::generate(&env);
- client.initialize(&token, &vec![&env, alice.clone()], &vec![&env, 10_000_u32]);
+ client.initialize(
+ &admin,
+ &token,
+ &vec![&env, alice.clone()],
+ &vec![&env, 10_000_u32],
+ );
let sac = StellarAssetClient::new(&env, &token);
sac.mint(&contract_id, &5_000);
@@ -279,12 +296,13 @@ fn test_distribute_single_beneficiary_gets_all() {
#[test]
fn test_distribute_can_be_called_multiple_times() {
- let (env, client, token, contract_id) = setup();
+ let (env, client, token, contract_id, admin) = setup();
let alice = Address::generate(&env);
let bob = Address::generate(&env);
let caller = Address::generate(&env);
client.initialize(
+ &admin,
&token,
&vec![&env, alice.clone(), bob.clone()],
&vec![&env, 5_000_u32, 5_000_u32],
@@ -306,12 +324,13 @@ fn test_distribute_can_be_called_multiple_times() {
#[test]
fn test_distribute_dust_goes_to_caller() {
- let (env, client, token, contract_id) = setup();
+ let (env, client, token, contract_id, admin) = setup();
let alice = Address::generate(&env);
let bob = Address::generate(&env);
let caller = Address::generate(&env);
client.initialize(
+ &admin,
&token,
&vec![&env, alice.clone(), bob.clone()],
&vec![&env, 5_000_u32, 5_000_u32],
@@ -333,11 +352,12 @@ fn test_distribute_dust_goes_to_caller() {
#[test]
fn test_get_share_for_beneficiary() {
- let (env, client, token, _) = setup();
+ let (env, client, token, _, admin) = setup();
let alice = Address::generate(&env);
let bob = Address::generate(&env);
client.initialize(
+ &admin,
&token,
&vec![&env, alice.clone(), bob.clone()],
&vec![&env, 6_000_u32, 4_000_u32],
@@ -349,12 +369,13 @@ fn test_get_share_for_beneficiary() {
#[test]
fn test_get_share_for_nonexistent_beneficiary() {
- let (env, client, token, _) = setup();
+ let (env, client, token, _, admin) = setup();
let alice = Address::generate(&env);
let bob = Address::generate(&env);
let charlie = Address::generate(&env);
client.initialize(
+ &admin,
&token,
&vec![&env, alice, bob],
&vec![&env, 5_000_u32, 5_000_u32],
@@ -367,7 +388,7 @@ fn test_get_share_for_nonexistent_beneficiary() {
#[test]
fn test_get_share_before_initialize() {
- let (env, client, _token, _) = setup();
+ let (env, client, _token, _, _) = setup();
let alice = Address::generate(&env);
let err = client.try_get_share(&alice).unwrap_err().unwrap();
@@ -376,10 +397,15 @@ fn test_get_share_before_initialize() {
#[test]
fn test_get_share_single_beneficiary() {
- let (env, client, token, _) = setup();
+ let (env, client, token, _, admin) = setup();
let alice = Address::generate(&env);
- client.initialize(&token, &vec![&env, alice.clone()], &vec![&env, 10_000_u32]);
+ client.initialize(
+ &admin,
+ &token,
+ &vec![&env, alice.clone()],
+ &vec![&env, 10_000_u32],
+ );
assert_eq!(client.get_share(&alice), 10_000_u32);
}
@@ -388,7 +414,7 @@ fn test_get_share_single_beneficiary() {
#[test]
fn test_distribute_royalties_exact_100_percent_success() {
- let (env, client, token, contract_id) = setup();
+ let (env, client, token, contract_id, admin) = setup();
let recipient_a = Address::generate(&env);
let recipient_b = Address::generate(&env);
let recipient_c = Address::generate(&env);
@@ -400,6 +426,7 @@ fn test_distribute_royalties_exact_100_percent_success() {
// Recipient C -> 20% (2_000 BPS)
// Total = 10_000 BPS (100%)
client.initialize(
+ &admin,
&token,
&vec![
&env,
@@ -453,7 +480,7 @@ fn test_distribute_royalties_exact_100_percent_success() {
#[test]
fn test_distribute_royalties_less_than_100_percent_fails() {
- let (env, client, token, contract_id) = setup();
+ let (env, client, token, contract_id, admin) = setup();
let recipient_a = Address::generate(&env);
let recipient_b = Address::generate(&env);
let recipient_c = Address::generate(&env);
@@ -463,6 +490,7 @@ fn test_distribute_royalties_less_than_100_percent_fails() {
// 40% + 30% + 20% = 90% (9_000 BPS)
let err = client
.try_initialize(
+ &admin,
&token,
&vec![
&env,
@@ -508,7 +536,7 @@ fn test_distribute_royalties_less_than_100_percent_fails() {
#[test]
fn test_distribute_royalties_greater_than_100_percent_fails() {
- let (env, client, token, contract_id) = setup();
+ let (env, client, token, contract_id, admin) = setup();
let recipient_a = Address::generate(&env);
let recipient_b = Address::generate(&env);
let recipient_c = Address::generate(&env);
@@ -518,6 +546,7 @@ fn test_distribute_royalties_greater_than_100_percent_fails() {
// 50% + 40% + 20% = 110% (11_000 BPS)
let err = client
.try_initialize(
+ &admin,
&token,
&vec![
&env,
@@ -563,7 +592,7 @@ fn test_distribute_royalties_greater_than_100_percent_fails() {
#[test]
fn test_distribute_royalties_max_recipients_100_percent_success() {
- let (env, client, token, contract_id) = setup();
+ let (env, client, token, contract_id, admin) = setup();
let caller = Address::generate(&env);
// Max beneficiaries supported is 20, total must equal 10_000 BPS (500 BPS each = 5% each)
@@ -578,7 +607,7 @@ fn test_distribute_royalties_max_recipients_100_percent_success() {
recipients.push(recipient);
}
- client.initialize(&token, &beneficiaries, &shares);
+ client.initialize(&admin, &token, &beneficiaries, &shares);
let sac = StellarAssetClient::new(&env, &token);
sac.mint(&contract_id, &20_000);
@@ -602,7 +631,7 @@ fn test_distribute_royalties_max_recipients_100_percent_success() {
#[test]
fn test_distribute_royalties_smallest_valid_percentages_100_percent_success() {
- let (env, client, token, contract_id) = setup();
+ let (env, client, token, contract_id, admin) = setup();
let caller = Address::generate(&env);
// Smallest BPS unit is 1 BPS (0.01%).
@@ -623,7 +652,7 @@ fn test_distribute_royalties_smallest_valid_percentages_100_percent_success() {
beneficiaries.push_back(large_recipient.clone());
shares.push_back(9_981_u32);
- client.initialize(&token, &beneficiaries, &shares);
+ client.initialize(&admin, &token, &beneficiaries, &shares);
let sac = StellarAssetClient::new(&env, &token);
// Mint 10,000,000 tokens so 1 BPS = 1,000 tokens cleanly
@@ -647,6 +676,272 @@ fn test_distribute_royalties_smallest_valid_percentages_100_percent_success() {
assert_eq!(tc.balance(&contract_id), 0);
}
+// ── update_royalty_split ─────────────────────────────────────────────
+
+#[test]
+fn test_update_royalty_split_by_admin() {
+ let (env, client, token, _, admin) = setup();
+ let alice = Address::generate(&env);
+ let bob = Address::generate(&env);
+ let charlie = Address::generate(&env);
+
+ // Initialize with initial config
+ client.initialize(
+ &admin,
+ &token,
+ &vec![&env, alice.clone(), bob.clone()],
+ &vec![&env, 5_000_u32, 5_000_u32],
+ );
+
+ // Update to new config
+ client.update_royalty_split(
+ &admin,
+ &vec![&env, alice.clone(), bob.clone(), charlie.clone()],
+ &vec![&env, 4_000_u32, 3_000_u32, 3_000_u32],
+ );
+
+ // Verify the update
+ let beneficiaries = client.get_beneficiaries();
+ assert_eq!(beneficiaries.len(), 3);
+ assert_eq!(beneficiaries.get(0).unwrap(), alice);
+ assert_eq!(beneficiaries.get(1).unwrap(), bob);
+ assert_eq!(beneficiaries.get(2).unwrap(), charlie);
+
+ let shares = client.get_shares();
+ assert_eq!(shares.get(0).unwrap(), 4_000_u32);
+ assert_eq!(shares.get(1).unwrap(), 3_000_u32);
+ assert_eq!(shares.get(2).unwrap(), 3_000_u32);
+}
+
+#[test]
+fn test_update_royalty_split_by_non_admin_fails() {
+ let (env, client, token, _, admin) = setup();
+ let non_admin = Address::generate(&env);
+ let alice = Address::generate(&env);
+ let bob = Address::generate(&env);
+
+ // Initialize with initial config
+ client.initialize(
+ &admin,
+ &token,
+ &vec![&env, alice.clone(), bob.clone()],
+ &vec![&env, 5_000_u32, 5_000_u32],
+ );
+
+ // Try to update as non-admin
+ let err = client
+ .try_update_royalty_split(
+ &non_admin,
+ &vec![&env, alice.clone(), bob.clone()],
+ &vec![&env, 7_000_u32, 3_000_u32],
+ )
+ .unwrap_err()
+ .unwrap();
+
+ assert_eq!(err, SplitterError::Unauthorized.into());
+
+ // Verify state didn't change
+ let shares = client.get_shares();
+ assert_eq!(shares.get(0).unwrap(), 5_000_u32);
+ assert_eq!(shares.get(1).unwrap(), 5_000_u32);
+}
+
+#[test]
+fn test_update_royalty_split_before_initialize_fails() {
+ let (env, client, _token, _, admin) = setup();
+ let alice = Address::generate(&env);
+ let bob = Address::generate(&env);
+
+ // Try to update before initialization
+ let err = client
+ .try_update_royalty_split(
+ &admin,
+ &vec![&env, alice, bob],
+ &vec![&env, 5_000_u32, 5_000_u32],
+ )
+ .unwrap_err()
+ .unwrap();
+
+ assert_eq!(err, SplitterError::NotInitialized.into());
+}
+
+#[test]
+fn test_update_royalty_split_with_invalid_shares_fails() {
+ let (env, client, token, _, admin) = setup();
+ let alice = Address::generate(&env);
+ let bob = Address::generate(&env);
+ let charlie = Address::generate(&env);
+
+ // Initialize
+ client.initialize(
+ &admin,
+ &token,
+ &vec![&env, alice.clone(), bob.clone()],
+ &vec![&env, 5_000_u32, 5_000_u32],
+ );
+
+ // Try to update with shares that don't sum to 10,000
+ let err = client
+ .try_update_royalty_split(
+ &admin,
+ &vec![&env, alice.clone(), bob.clone(), charlie.clone()],
+ &vec![&env, 4_000_u32, 3_000_u32, 2_000_u32], // Sums to 9,000
+ )
+ .unwrap_err()
+ .unwrap();
+
+ assert_eq!(err, SplitterError::InvalidShares.into());
+
+ // Verify state didn't change
+ let shares = client.get_shares();
+ assert_eq!(shares.get(0).unwrap(), 5_000_u32);
+ assert_eq!(shares.get(1).unwrap(), 5_000_u32);
+}
+
+#[test]
+fn test_update_royalty_split_with_empty_beneficiaries_fails() {
+ let (env, client, token, _, admin) = setup();
+ let alice = Address::generate(&env);
+ let bob = Address::generate(&env);
+
+ // Initialize
+ client.initialize(
+ &admin,
+ &token,
+ &vec![&env, alice, bob],
+ &vec![&env, 5_000_u32, 5_000_u32],
+ );
+
+ // Try to update with empty beneficiaries
+ let err = client
+ .try_update_royalty_split(&admin, &vec![&env], &vec![&env])
+ .unwrap_err()
+ .unwrap();
+
+ assert_eq!(err, SplitterError::NoBeneficiaries.into());
+}
+
+#[test]
+fn test_update_royalty_split_with_too_many_beneficiaries_fails() {
+ let (env, client, token, _, admin) = setup();
+ let alice = Address::generate(&env);
+ let bob = Address::generate(&env);
+
+ // Initialize
+ client.initialize(
+ &admin,
+ &token,
+ &vec![&env, alice.clone(), bob.clone()],
+ &vec![&env, 5_000_u32, 5_000_u32],
+ );
+
+ // Create 21 beneficiaries (MAX_BENEFICIARIES is 20)
+ let mut beneficiaries = vec![&env];
+ let mut shares = vec![&env];
+ for _ in 0..21 {
+ beneficiaries.push_back(Address::generate(&env));
+ shares.push_back(1_u32);
+ }
+
+ // Try to update with too many beneficiaries
+ let err = client
+ .try_update_royalty_split(&admin, &beneficiaries, &shares)
+ .unwrap_err()
+ .unwrap();
+
+ assert_eq!(err, SplitterError::TooManyBeneficiaries.into());
+}
+
+#[test]
+fn test_update_royalty_split_length_mismatch_fails() {
+ let (env, client, token, _, admin) = setup();
+ let alice = Address::generate(&env);
+ let bob = Address::generate(&env);
+
+ // Initialize
+ client.initialize(
+ &admin,
+ &token,
+ &vec![&env, alice.clone(), bob.clone()],
+ &vec![&env, 5_000_u32, 5_000_u32],
+ );
+
+ // Try to update with mismatched lengths
+ let err = client
+ .try_update_royalty_split(
+ &admin,
+ &vec![&env, alice.clone(), bob.clone()],
+ &vec![&env, 7_000_u32], // Only 1 share for 2 beneficiaries
+ )
+ .unwrap_err()
+ .unwrap();
+
+ assert_eq!(err, SplitterError::LengthMismatch.into());
+}
+
+#[test]
+fn test_update_royalty_split_multiple_times() {
+ let (env, client, token, contract_id, admin) = setup();
+ let alice = Address::generate(&env);
+ let bob = Address::generate(&env);
+ let charlie = Address::generate(&env);
+ let dave = Address::generate(&env);
+
+ // Initialize with 2 beneficiaries
+ client.initialize(
+ &admin,
+ &token,
+ &vec![&env, alice.clone(), bob.clone()],
+ &vec![&env, 5_000_u32, 5_000_u32],
+ );
+
+ // First update - 3 beneficiaries
+ client.update_royalty_split(
+ &admin,
+ &vec![&env, alice.clone(), bob.clone(), charlie.clone()],
+ &vec![&env, 4_000_u32, 3_000_u32, 3_000_u32],
+ );
+
+ let shares = client.get_shares();
+ assert_eq!(shares.len(), 3);
+ assert_eq!(shares.get(0).unwrap(), 4_000_u32);
+ assert_eq!(shares.get(1).unwrap(), 3_000_u32);
+ assert_eq!(shares.get(2).unwrap(), 3_000_u32);
+
+ // Second update - 4 beneficiaries
+ client.update_royalty_split(
+ &admin,
+ &vec![
+ &env,
+ alice.clone(),
+ bob.clone(),
+ charlie.clone(),
+ dave.clone(),
+ ],
+ &vec![&env, 2_500_u32, 2_500_u32, 2_500_u32, 2_500_u32],
+ );
+
+ let shares = client.get_shares();
+ assert_eq!(shares.len(), 4);
+ assert_eq!(shares.get(0).unwrap(), 2_500_u32);
+ assert_eq!(shares.get(1).unwrap(), 2_500_u32);
+ assert_eq!(shares.get(2).unwrap(), 2_500_u32);
+ assert_eq!(shares.get(3).unwrap(), 2_500_u32);
+
+ // Verify distribution still works
+ let caller = Address::generate(&env);
+ let sac = StellarAssetClient::new(&env, &token);
+ // Use contract_id from setup instead of env.current_contract_address()
+ sac.mint(&contract_id, &1_000_000);
+ client.distribute(&token, &caller);
+
+ let tc = TokenClient::new(&env, &token);
+ assert_eq!(tc.balance(&alice), 250_000);
+ assert_eq!(tc.balance(&bob), 250_000);
+ assert_eq!(tc.balance(&charlie), 250_000);
+ assert_eq!(tc.balance(&dave), 250_000);
+}
+
// ── Invalid Recipient Address Regression Tests ─────────────────────────────
#[test]
@@ -660,7 +955,7 @@ fn test_distribute_royalties_invalid_strkey_recipient_fails() {
#[test]
#[should_panic]
fn test_distribute_royalties_invalid_recipient_first_position_fails() {
- let (env, client, token, _contract_id) = setup();
+ let (env, client, token, _, admin) = setup();
let valid_b = Address::generate(&env);
let valid_c = Address::generate(&env);
@@ -668,6 +963,7 @@ fn test_distribute_royalties_invalid_recipient_first_position_fails() {
let invalid_a = Address::from_string_bytes(&invalid_bytes);
client.initialize(
+ &admin,
&token,
&vec![&env, invalid_a, valid_b, valid_c],
&vec![&env, 5_000_u32, 3_000_u32, 2_000_u32],
@@ -677,7 +973,7 @@ fn test_distribute_royalties_invalid_recipient_first_position_fails() {
#[test]
#[should_panic]
fn test_distribute_royalties_invalid_recipient_middle_position_fails() {
- let (env, client, token, _contract_id) = setup();
+ let (env, client, token, _, admin) = setup();
let valid_a = Address::generate(&env);
let valid_c = Address::generate(&env);
@@ -685,6 +981,7 @@ fn test_distribute_royalties_invalid_recipient_middle_position_fails() {
let invalid_b = Address::from_string_bytes(&invalid_bytes);
client.initialize(
+ &admin,
&token,
&vec![&env, valid_a, invalid_b, valid_c],
&vec![&env, 5_000_u32, 3_000_u32, 2_000_u32],
@@ -694,7 +991,7 @@ fn test_distribute_royalties_invalid_recipient_middle_position_fails() {
#[test]
#[should_panic]
fn test_distribute_royalties_invalid_recipient_final_position_fails() {
- let (env, client, token, _contract_id) = setup();
+ let (env, client, token, _, admin) = setup();
let valid_a = Address::generate(&env);
let valid_b = Address::generate(&env);
@@ -702,8 +999,9 @@ fn test_distribute_royalties_invalid_recipient_final_position_fails() {
let invalid_c = Address::from_string_bytes(&invalid_bytes);
client.initialize(
+ &admin,
&token,
&vec![&env, valid_a, valid_b, invalid_c],
&vec![&env, 5_000_u32, 3_000_u32, 2_000_u32],
);
-}
+}
\ No newline at end of file
diff --git a/contracts/royalty-splitter/src/types.rs b/contracts/royalty-splitter/src/types.rs
index 3c2656e..73125ee 100644
--- a/contracts/royalty-splitter/src/types.rs
+++ b/contracts/royalty-splitter/src/types.rs
@@ -12,4 +12,5 @@ pub enum SplitterError {
InvalidShares = 5,
TooManyBeneficiaries = 6,
BeneficiaryNotFound = 7,
+ Unauthorized = 8,
}