From 15be2fc644490a60456cb50c39702219c1cd9a6c Mon Sep 17 00:00:00 2001 From: shogun444 Date: Mon, 1 Jun 2026 23:23:43 +0530 Subject: [PATCH 1/3] test: cover supply_cap boundary semantics in deposit flow --- src/lib.rs | 167 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 166 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index a4eceed9..f498a48f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6498,4 +6498,169 @@ mod issue_370_373_tests { i128::MIN ); } -} \ No newline at end of file +} + +#[cfg(test)] +mod issue_414_supply_cap_tests { + use super::*; + use soroban_sdk::{testutils::Address as _, token, Address, Env, Symbol}; + + fn setup_with_payment_token( + mint_amount: i128, + ) -> ( + Env, + RevoraRevenueShareClient<'static>, + Address, + Symbol, + Address, + Address, + ) { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register_contract(None, RevoraRevenueShare); + let client = RevoraRevenueShareClient::new(&env, &contract_id); + + let issuer = Address::generate(&env); + let namespace = Symbol::new(&env, "def"); + let token_addr = Address::generate(&env); + + let token_admin = Address::generate(&env); + let payment_token = env.register_stellar_asset_contract(token_admin.clone()); + let payment_token_admin = token::StellarAssetClient::new(&env, &payment_token); + payment_token_admin.mint(&issuer, &mint_amount); + + (env, client, issuer, namespace, token_addr, payment_token) + } + + #[test] + fn supply_cap_zero_is_unset_and_not_enforced() { + let (_env, client, issuer, namespace, token_addr, payment_token) = + setup_with_payment_token(2_000_000); + + assert_eq!( + client + .try_register_offering(&issuer, &namespace, &token_addr, &1_000, &payment_token, &0), + Ok(Ok(())) + ); + assert_eq!(client.get_supply_cap(&issuer, &namespace, &token_addr), 0); + + assert_eq!( + client + .try_deposit_revenue(&issuer, &namespace, &token_addr, &payment_token, &700_000, &1), + Ok(Ok(())) + ); + assert_eq!( + client + .try_deposit_revenue(&issuer, &namespace, &token_addr, &payment_token, &700_000, &2), + Ok(Ok(())) + ); + assert_eq!( + client + .try_deposit_revenue(&issuer, &namespace, &token_addr, &payment_token, &600_000, &3), + Ok(Ok(())) + ); + } + + #[test] + fn supply_cap_one_allows_exact_cap_and_rejects_next_unit() { + let (_env, client, issuer, namespace, token_addr, payment_token) = + setup_with_payment_token(10); + + assert_eq!( + client + .try_register_offering(&issuer, &namespace, &token_addr, &1_000, &payment_token, &1), + Ok(Ok(())) + ); + assert_eq!(client.get_supply_cap(&issuer, &namespace, &token_addr), 1); + + assert_eq!( + client.try_deposit_revenue(&issuer, &namespace, &token_addr, &payment_token, &1, &1), + Ok(Ok(())) + ); + assert_eq!( + client.try_deposit_revenue(&issuer, &namespace, &token_addr, &payment_token, &1, &2), + Err(Ok(RevoraError::SupplyCapExceeded)) + ); + } + + #[test] + fn supply_cap_readable_large_boundary_enforced() { + let (_env, client, issuer, namespace, token_addr, payment_token) = + setup_with_payment_token(2_000_000); + let cap = 1_000_000_i128; + + assert_eq!( + client.try_register_offering( + &issuer, + &namespace, + &token_addr, + &1_000, + &payment_token, + &cap + ), + Ok(Ok(())) + ); + assert_eq!(client.get_supply_cap(&issuer, &namespace, &token_addr), cap); + + assert_eq!( + client.try_deposit_revenue(&issuer, &namespace, &token_addr, &payment_token, &cap, &1), + Ok(Ok(())) + ); + assert_eq!( + client.try_deposit_revenue(&issuer, &namespace, &token_addr, &payment_token, &1, &2), + Err(Ok(RevoraError::SupplyCapExceeded)) + ); + } + + #[test] + fn negative_supply_cap_rejected_on_register() { + let (_env, client, issuer, namespace, token_addr, payment_token) = + setup_with_payment_token(10); + + assert_eq!( + client.try_register_offering( + &issuer, + &namespace, + &token_addr, + &1_000, + &payment_token, + &-1_i128 + ), + Err(Ok(RevoraError::InvalidAmount)) + ); + } + + #[test] + fn supply_cap_saturation_near_i128_max_is_safe() { + let (env, client, issuer, namespace, token_addr, payment_token) = + setup_with_payment_token(i128::MAX); + + let cap = i128::MAX - 2; + assert_eq!( + client.try_register_offering( + &issuer, + &namespace, + &token_addr, + &1_000, + &payment_token, + &cap + ), + Ok(Ok(())) + ); + + RevoraRevenueShare::test_insert_period( + env.clone(), + issuer.clone(), + namespace.clone(), + token_addr.clone(), + 1, + i128::MAX - 3, + ); + + assert_eq!( + client.try_deposit_revenue(&issuer, &namespace, &token_addr, &payment_token, &10, &2), + Err(Ok(RevoraError::SupplyCapExceeded)) + ); + } +} From 67684c7d95e0f2dc06f19c2c0d003a890e4aff50 Mon Sep 17 00:00:00 2001 From: shogun444 Date: Mon, 1 Jun 2026 23:39:39 +0530 Subject: [PATCH 2/3] test: cover supply_cap boundary semantics in deposit flow --- src/lib.rs | 36 ++++++----- src/test_claim_transfer_fail.rs | 96 +++++++++-------------------- src/test_multisig_gas.rs | 106 +++++++++++++++++++++----------- src/vesting.rs | 14 +++-- 4 files changed, 129 insertions(+), 123 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f498a48f..b4f4b9c1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4331,6 +4331,7 @@ impl RevoraRevenueShare { period_id: u64, ) -> Result<(), RevoraError> { Self::require_not_frozen(&env)?; + Self::require_not_paused(&env)?; issuer.require_auth(); // Input validation (#35): reject zero/invalid period_id and non-positive amounts. @@ -4372,6 +4373,7 @@ impl RevoraRevenueShare { snapshot_reference: u64, ) -> Result<(), RevoraError> { Self::require_not_frozen(&env)?; + Self::require_not_paused(&env)?; issuer.require_auth(); // 0. Validate snapshot reference using Negative Amount Validation Matrix (#163) @@ -4694,7 +4696,7 @@ impl RevoraRevenueShare { .get(&DataKey::HolderShare(offering_id.clone(), holder.clone())) .unwrap_or(0); - let new_total = current_total.saturating_sub(old_share).saturating_add(*share_bps); + let new_total = current_total.saturating_sub(old_share).saturating_add(share_bps); if new_total > 10_000 { return Err(RevoraError::InvalidShareBps); } @@ -4705,7 +4707,7 @@ impl RevoraRevenueShare { .set(&DataKey::HolderShare(offering_id.clone(), holder.clone()), &share_bps); current_total = new_total; - added_bps = added_bps.saturating_add(*share_bps); + added_bps = added_bps.saturating_add(share_bps); } // Update snapshot metadata. @@ -6509,6 +6511,7 @@ mod issue_414_supply_cap_tests { mint_amount: i128, ) -> ( Env, + Address, RevoraRevenueShareClient<'static>, Address, Symbol, @@ -6530,12 +6533,12 @@ mod issue_414_supply_cap_tests { let payment_token_admin = token::StellarAssetClient::new(&env, &payment_token); payment_token_admin.mint(&issuer, &mint_amount); - (env, client, issuer, namespace, token_addr, payment_token) + (env, contract_id, client, issuer, namespace, token_addr, payment_token) } #[test] fn supply_cap_zero_is_unset_and_not_enforced() { - let (_env, client, issuer, namespace, token_addr, payment_token) = + let (_env, _contract_id, client, issuer, namespace, token_addr, payment_token) = setup_with_payment_token(2_000_000); assert_eq!( @@ -6564,7 +6567,7 @@ mod issue_414_supply_cap_tests { #[test] fn supply_cap_one_allows_exact_cap_and_rejects_next_unit() { - let (_env, client, issuer, namespace, token_addr, payment_token) = + let (_env, _contract_id, client, issuer, namespace, token_addr, payment_token) = setup_with_payment_token(10); assert_eq!( @@ -6586,7 +6589,7 @@ mod issue_414_supply_cap_tests { #[test] fn supply_cap_readable_large_boundary_enforced() { - let (_env, client, issuer, namespace, token_addr, payment_token) = + let (_env, _contract_id, client, issuer, namespace, token_addr, payment_token) = setup_with_payment_token(2_000_000); let cap = 1_000_000_i128; @@ -6615,7 +6618,7 @@ mod issue_414_supply_cap_tests { #[test] fn negative_supply_cap_rejected_on_register() { - let (_env, client, issuer, namespace, token_addr, payment_token) = + let (_env, _contract_id, client, issuer, namespace, token_addr, payment_token) = setup_with_payment_token(10); assert_eq!( @@ -6633,7 +6636,7 @@ mod issue_414_supply_cap_tests { #[test] fn supply_cap_saturation_near_i128_max_is_safe() { - let (env, client, issuer, namespace, token_addr, payment_token) = + let (_env, _contract_id, client, issuer, namespace, token_addr, payment_token) = setup_with_payment_token(i128::MAX); let cap = i128::MAX - 2; @@ -6649,13 +6652,16 @@ mod issue_414_supply_cap_tests { Ok(Ok(())) ); - RevoraRevenueShare::test_insert_period( - env.clone(), - issuer.clone(), - namespace.clone(), - token_addr.clone(), - 1, - i128::MAX - 3, + assert_eq!( + client.try_deposit_revenue( + &issuer, + &namespace, + &token_addr, + &payment_token, + &(i128::MAX - 3), + &1 + ), + Ok(Ok(())) ); assert_eq!( diff --git a/src/test_claim_transfer_fail.rs b/src/test_claim_transfer_fail.rs index fdc4a334..3340085c 100644 --- a/src/test_claim_transfer_fail.rs +++ b/src/test_claim_transfer_fail.rs @@ -173,6 +173,24 @@ fn deploy_failing_token(env: &Env) -> (Address, FailingTransferTokenClient<'stat (id, client) } +fn pending_periods( + env: &Env, + revora_id: &Address, + issuer: &Address, + offering_token: &Address, + holder: &Address, +) -> soroban_sdk::Vec { + env.as_contract(revora_id, || { + RevoraRevenueShare::get_pending_periods( + env.clone(), + issuer.clone(), + symbol_short!("def"), + offering_token.clone(), + holder.clone(), + ) + }) +} + /// Full setup for claim-failure tests. /// /// - Registers an offering with `FailingTransferToken` as payment token. @@ -250,27 +268,15 @@ fn claim_transfer_fail_returns_transfer_failed() { /// `LastClaimedIdx` is NOT advanced when claim transfer fails. #[test] fn claim_transfer_fail_does_not_advance_last_claimed_idx() { - let (env, _revora_id, revora, _fail_token_id, _fail_token, issuer, offering_token, holder) = + let (env, revora_id, revora, _fail_token_id, _fail_token, issuer, offering_token, holder) = setup_claim_fail(); - let pending_before = RevoraRevenueShare::get_pending_periods( - env.clone(), - issuer.clone(), - symbol_short!("def"), - offering_token.clone(), - holder.clone(), - ); + let pending_before = pending_periods(&env, &revora_id, &issuer, &offering_token, &holder); assert_eq!(pending_before.len(), 1, "should have 1 pending period before failed claim"); let _ = revora.try_claim(&holder, &issuer, &symbol_short!("def"), &offering_token, &50); - let pending_after = RevoraRevenueShare::get_pending_periods( - env.clone(), - issuer.clone(), - symbol_short!("def"), - offering_token.clone(), - holder.clone(), - ); + let pending_after = pending_periods(&env, &revora_id, &issuer, &offering_token, &holder); assert_eq!( pending_after.len(), pending_before.len(), @@ -316,7 +322,7 @@ fn claim_transfer_fail_contract_balance_unchanged() { /// After a failed claim, the holder can retry and succeed once the token issue is resolved. #[test] fn claim_transfer_fail_then_retry_succeeds() { - let (env, _revora_id, revora, _fail_token_id, fail_token, issuer, offering_token, holder) = + let (env, revora_id, revora, _fail_token_id, fail_token, issuer, offering_token, holder) = setup_claim_fail(); // First attempt fails @@ -333,13 +339,7 @@ fn claim_transfer_fail_then_retry_succeeds() { assert_eq!(r2.unwrap().unwrap(), 100_000, "holder should receive full payout on retry"); // Pending periods now empty - let pending = RevoraRevenueShare::get_pending_periods( - env.clone(), - issuer.clone(), - symbol_short!("def"), - offering_token.clone(), - holder.clone(), - ); + let pending = pending_periods(&env, &revora_id, &issuer, &offering_token, &holder); assert_eq!(pending.len(), 0, "all periods should be claimed after successful retry"); } @@ -374,13 +374,7 @@ fn claim_transfer_fail_multi_period_no_partial_state() { // Re-arm fail mode for claim direction fail_token.set_fail_from(&revora_id); - let pending_before = RevoraRevenueShare::get_pending_periods( - env.clone(), - issuer.clone(), - symbol_short!("def"), - offering_token.clone(), - holder.clone(), - ); + let pending_before = pending_periods(&env, &revora_id, &issuer, &offering_token, &holder); assert_eq!(pending_before.len(), 3); // Attempt to claim all 3 — transfer fails @@ -388,13 +382,7 @@ fn claim_transfer_fail_multi_period_no_partial_state() { assert!(matches!(result.err(), Some(Ok(RevoraError::TransferFailed)))); // All 3 periods still pending — no partial state - let pending_after = RevoraRevenueShare::get_pending_periods( - env.clone(), - issuer.clone(), - symbol_short!("def"), - offering_token.clone(), - holder.clone(), - ); + let pending_after = pending_periods(&env, &revora_id, &issuer, &offering_token, &holder); assert_eq!( pending_after.len(), 3, @@ -434,30 +422,18 @@ fn claim_transfer_fail_does_not_affect_other_holder_state() { assert!(matches!(r1.err(), Some(Ok(RevoraError::TransferFailed)))); // holder2 pending state is independent and unchanged - let pending_h2 = RevoraRevenueShare::get_pending_periods( - env.clone(), - issuer.clone(), - symbol_short!("def"), - offering_token.clone(), - holder2.clone(), - ); + let pending_h2 = pending_periods(&env, &revora_id, &issuer, &offering_token, &holder2); assert_eq!(pending_h2.len(), 2, "holder2 should still have 2 pending periods"); // holder1 pending state also unchanged - let pending_h1 = RevoraRevenueShare::get_pending_periods( - env.clone(), - issuer.clone(), - symbol_short!("def"), - offering_token.clone(), - holder.clone(), - ); + let pending_h1 = pending_periods(&env, &revora_id, &issuer, &offering_token, &holder); assert_eq!(pending_h1.len(), 2, "holder1 should still have 2 pending periods"); } /// A failed claim on one offering does not affect a sibling offering's state. #[test] fn claim_transfer_fail_does_not_affect_sibling_offering() { - let (env, _revora_id, revora, _fail_token_id, _fail_token, issuer, offering_token_a, holder) = + let (env, revora_id, revora, _fail_token_id, _fail_token, issuer, offering_token_a, holder) = setup_claim_fail(); // Register a second offering with a normal Stellar asset token @@ -494,22 +470,10 @@ fn claim_transfer_fail_does_not_affect_sibling_offering() { assert_eq!(r_b.unwrap().unwrap(), 100_000); // Offering A: period 1 still pending - let pending_a = RevoraRevenueShare::get_pending_periods( - env.clone(), - issuer.clone(), - symbol_short!("def"), - offering_token_a.clone(), - holder.clone(), - ); + let pending_a = pending_periods(&env, &revora_id, &issuer, &offering_token_a, &holder); assert_eq!(pending_a.len(), 1, "offering A period must remain pending"); // Offering B: no pending periods - let pending_b = RevoraRevenueShare::get_pending_periods( - env.clone(), - issuer.clone(), - symbol_short!("def"), - offering_token_b.clone(), - holder.clone(), - ); + let pending_b = pending_periods(&env, &revora_id, &issuer, &offering_token_b, &holder); assert_eq!(pending_b.len(), 0, "offering B must be fully claimed"); } diff --git a/src/test_multisig_gas.rs b/src/test_multisig_gas.rs index 2d3ca249..dda9ec50 100644 --- a/src/test_multisig_gas.rs +++ b/src/test_multisig_gas.rs @@ -88,13 +88,15 @@ fn setup_max_multisig() -> (Env, Address, RevoraRevenueShareClient<'static>, Add // Majority threshold; duration = 1 day. let threshold = RevoraRevenueShare::MAX_MULTISIG_OWNERS / 2 + 1; // 11 - RevoraRevenueShare::init_multisig( - env.clone(), - admin.clone(), - owners.clone(), - threshold, - 86_400u64, - ) + env.as_contract(&id, || { + RevoraRevenueShare::init_multisig( + env.clone(), + admin.clone(), + owners.clone(), + threshold, + 86_400u64, + ) + }) .unwrap(); (env, id, client, admin, owners) @@ -104,18 +106,24 @@ fn setup_max_multisig() -> (Env, Address, RevoraRevenueShareClient<'static>, Add /// Returns the proposal id ready for `execute_action`. fn propose_and_approve( env: &Env, + contract_id: &Address, owners: &Vec
, threshold: u32, action: ProposalAction, ) -> u32 { // owners[0] proposes (counts as first approval automatically). let proposer = owners.get(0).unwrap(); - let proposal_id = RevoraRevenueShare::propose_action(env.clone(), proposer, action).unwrap(); + let proposal_id = env + .as_contract(contract_id, || RevoraRevenueShare::propose_action(env.clone(), proposer, action)) + .unwrap(); // Collect remaining approvals up to threshold. for i in 1..threshold { let approver = owners.get(i).unwrap(); - RevoraRevenueShare::approve_action(env.clone(), approver, proposal_id).unwrap(); + env.as_contract(contract_id, || { + RevoraRevenueShare::approve_action(env.clone(), approver, proposal_id) + }) + .unwrap(); } proposal_id @@ -141,11 +149,12 @@ fn execute_remove_owner_at_max_owners_within_budget() { let target = owners.get(RevoraRevenueShare::MAX_MULTISIG_OWNERS - 1).unwrap(); let action = ProposalAction::RemoveOwner(target); - let proposal_id = propose_and_approve(&env, &owners, threshold, action); + let proposal_id = propose_and_approve(&env, &id, &owners, threshold, action); let executor = owners.get(0).unwrap(); // Must complete without panic or resource exhaustion. - RevoraRevenueShare::execute_action(env.clone(), executor, proposal_id).unwrap(); + env.as_contract(&id, || RevoraRevenueShare::execute_action(env.clone(), executor, proposal_id)) + .unwrap(); // Functional correctness: owner count decreased by 1. assert_eq!( @@ -172,22 +181,25 @@ fn execute_add_owner_at_cap_minus_one_within_budget() { owners.push_back(Address::generate(&env)); } let threshold = count / 2 + 1; // 10 - RevoraRevenueShare::init_multisig( - env.clone(), - admin.clone(), - owners.clone(), - threshold, - 86_400u64, - ) + env.as_contract(&id, || { + RevoraRevenueShare::init_multisig( + env.clone(), + admin.clone(), + owners.clone(), + threshold, + 86_400u64, + ) + }) .unwrap(); let new_owner = Address::generate(&env); let action = ProposalAction::AddOwner(new_owner.clone()); - let proposal_id = propose_and_approve(&env, &owners, threshold, action); + let proposal_id = propose_and_approve(&env, &id, &owners, threshold, action); let executor = owners.get(0).unwrap(); // Must complete without panic or resource exhaustion. - RevoraRevenueShare::execute_action(env.clone(), executor, proposal_id).unwrap(); + env.as_contract(&id, || RevoraRevenueShare::execute_action(env.clone(), executor, proposal_id)) + .unwrap(); // Functional correctness: owner count is now MAX. let final_owners = read_owners(&env, &id); @@ -210,10 +222,11 @@ fn execute_add_owner_at_max_returns_limit_reached() { let threshold = RevoraRevenueShare::MAX_MULTISIG_OWNERS / 2 + 1; let new_owner = Address::generate(&env); let action = ProposalAction::AddOwner(new_owner); - let proposal_id = propose_and_approve(&env, &owners, threshold, action); + let proposal_id = propose_and_approve(&env, &id, &owners, threshold, action); let executor = owners.get(0).unwrap(); - let result = RevoraRevenueShare::execute_action(env.clone(), executor, proposal_id); + let result = + env.as_contract(&id, || RevoraRevenueShare::execute_action(env.clone(), executor, proposal_id)); assert_eq!( result, @@ -253,17 +266,30 @@ fn execute_remove_owner_below_threshold_returns_limit_reached() { owners.push_back(owner1.clone()); owners.push_back(owner2.clone()); owners.push_back(owner3.clone()); - RevoraRevenueShare::init_multisig(env.clone(), admin.clone(), owners.clone(), 3u32, 86_400u64) - .unwrap(); + env.as_contract(&id, || { + RevoraRevenueShare::init_multisig( + env.clone(), + admin.clone(), + owners.clone(), + 3u32, + 86_400u64, + ) + }) + .unwrap(); // All 3 must approve to meet threshold = 3. let action = ProposalAction::RemoveOwner(owner3.clone()); - let proposal_id = - RevoraRevenueShare::propose_action(env.clone(), owner1.clone(), action).unwrap(); - RevoraRevenueShare::approve_action(env.clone(), owner2.clone(), proposal_id).unwrap(); - RevoraRevenueShare::approve_action(env.clone(), owner3.clone(), proposal_id).unwrap(); + let proposal_id = env + .as_contract(&id, || RevoraRevenueShare::propose_action(env.clone(), owner1.clone(), action)) + .unwrap(); + env.as_contract(&id, || RevoraRevenueShare::approve_action(env.clone(), owner2.clone(), proposal_id)) + .unwrap(); + env.as_contract(&id, || RevoraRevenueShare::approve_action(env.clone(), owner3.clone(), proposal_id)) + .unwrap(); - let result = RevoraRevenueShare::execute_action(env.clone(), owner1.clone(), proposal_id); + let result = env.as_contract(&id, || { + RevoraRevenueShare::execute_action(env.clone(), owner1.clone(), proposal_id) + }); assert_eq!( result, @@ -294,11 +320,12 @@ fn execute_action_non_owner_returns_not_authorized() { let threshold = RevoraRevenueShare::MAX_MULTISIG_OWNERS / 2 + 1; let target = owners.get(RevoraRevenueShare::MAX_MULTISIG_OWNERS - 1).unwrap(); let action = ProposalAction::RemoveOwner(target); - let proposal_id = propose_and_approve(&env, &owners, threshold, action); + let proposal_id = propose_and_approve(&env, &id, &owners, threshold, action); // outsider is not in the owners list let outsider = Address::generate(&env); - let result = RevoraRevenueShare::execute_action(env.clone(), outsider, proposal_id); + let result = + env.as_contract(&id, || RevoraRevenueShare::execute_action(env.clone(), outsider, proposal_id)); assert_eq!( result, @@ -320,7 +347,7 @@ fn execute_action_expired_proposal_returns_proposal_expired() { let threshold = RevoraRevenueShare::MAX_MULTISIG_OWNERS / 2 + 1; let target = owners.get(RevoraRevenueShare::MAX_MULTISIG_OWNERS - 1).unwrap(); let action = ProposalAction::RemoveOwner(target); - let proposal_id = propose_and_approve(&env, &owners, threshold, action); + let proposal_id = propose_and_approve(&env, &id, &owners, threshold, action); // Advance ledger time past the 1-day duration. env.ledger().with_mut(|li| { @@ -328,7 +355,8 @@ fn execute_action_expired_proposal_returns_proposal_expired() { }); let executor = owners.get(0).unwrap(); - let result = RevoraRevenueShare::execute_action(env.clone(), executor, proposal_id); + let result = + env.as_contract(&id, || RevoraRevenueShare::execute_action(env.clone(), executor, proposal_id)); assert_eq!( result, @@ -344,20 +372,24 @@ fn execute_action_expired_proposal_returns_proposal_expired() { /// `execute_action` on an already-executed proposal returns `LimitReached`. #[test] fn execute_action_already_executed_returns_limit_reached() { - let (env, _id, _client, _admin, owners) = setup_max_multisig(); + let (env, id, _client, _admin, owners) = setup_max_multisig(); let threshold = RevoraRevenueShare::MAX_MULTISIG_OWNERS / 2 + 1; let target = owners.get(RevoraRevenueShare::MAX_MULTISIG_OWNERS - 1).unwrap(); let action = ProposalAction::RemoveOwner(target); - let proposal_id = propose_and_approve(&env, &owners, threshold, action); + let proposal_id = propose_and_approve(&env, &id, &owners, threshold, action); let executor = owners.get(0).unwrap(); // First execution succeeds. - RevoraRevenueShare::execute_action(env.clone(), executor.clone(), proposal_id).unwrap(); + env.as_contract(&id, || { + RevoraRevenueShare::execute_action(env.clone(), executor.clone(), proposal_id) + }) + .unwrap(); // Second execution must fail. - let result = RevoraRevenueShare::execute_action(env.clone(), executor, proposal_id); + let result = + env.as_contract(&id, || RevoraRevenueShare::execute_action(env.clone(), executor, proposal_id)); assert_eq!( result, Err(RevoraError::LimitReached), diff --git a/src/vesting.rs b/src/vesting.rs index 68c18cab..90fcdeb0 100644 --- a/src/vesting.rs +++ b/src/vesting.rs @@ -107,9 +107,9 @@ impl VestingContract { } let schedule = VestingSchedule { - issuer, + issuer: issuer.clone(), beneficiary: beneficiary.clone(), - token, + token: token.clone(), total_amount, cliff_ts, start_ts, @@ -218,7 +218,7 @@ pub fn migrate_offering_schedules( return Ok(Vec::new(env)); } - let mut beneficiaries = Vec::new(env); + let mut beneficiaries: Vec
= Vec::new(env); for i in 0..count { if let Some(beneficiary) = env.storage().persistent().get(&VestingKey::OfferingScheduleItem(offering_id.clone(), i)) @@ -237,7 +237,9 @@ pub fn migrate_offering_schedules( // First pass: validate that no schedule is pre-cliff. for beneficiary in beneficiaries.iter() { - if let Some(schedule) = env.storage().persistent().get(&VestingKey::Schedule(beneficiary.clone())) { + let schedule: Option = + env.storage().persistent().get(&VestingKey::Schedule(beneficiary.clone())); + if let Some(schedule) = schedule { if schedule.issuer == offering_id.issuer && schedule.token == offering_id.token { if now < schedule.cliff_ts { return Err(VestingError::SchedulePreCliff); @@ -248,7 +250,9 @@ pub fn migrate_offering_schedules( // Second pass: migrate matching schedules and rebuild the beneficiary index. for beneficiary in beneficiaries.iter() { - if let Some(mut schedule) = env.storage().persistent().get(&VestingKey::Schedule(beneficiary.clone())) { + let schedule: Option = + env.storage().persistent().get(&VestingKey::Schedule(beneficiary.clone())); + if let Some(mut schedule) = schedule { if schedule.issuer == offering_id.issuer && schedule.token == offering_id.token { schedule.issuer = new_issuer.clone(); env.storage().persistent().set(&VestingKey::Schedule(beneficiary.clone()), &schedule); From 6c386394cd38d230291c29f3c431ac597d4fa368 Mon Sep 17 00:00:00 2001 From: shogun444 Date: Mon, 1 Jun 2026 23:41:33 +0530 Subject: [PATCH 3/3] test: cover supply_cap boundary semantics in deposit flow --- src/lib.rs | 88 ++++++++++++++++++++++++++++------------ src/test_multisig_gas.rs | 42 ++++++++++++------- src/test_pause_tiers.rs | 6 +-- src/vesting.rs | 24 +++++++---- 4 files changed, 106 insertions(+), 54 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b4f4b9c1..9ed2bcf2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![no_std] +#![no_std] #![deny(unsafe_code)] #![allow(dead_code)] #![allow(unused_variables)] @@ -37,7 +37,8 @@ clippy::manual_let_else, clippy::empty_line_after_doc_comments, clippy::doc_lazy_continuation, - clippy::unnecessary_lazy_evaluations + clippy::unnecessary_lazy_evaluations, + clippy::enum_variant_names )] use soroban_sdk::{ contract, contracterror, contractimpl, contracttype, symbol_short, token, xdr::ToXdr, Address, @@ -185,6 +186,7 @@ mod test_pause_tiers; #[contracttype] #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[repr(u32)] +#[allow(clippy::enum_variant_names)] pub enum PauseState { NotPaused = 0, SoftPaused = 1, @@ -1193,7 +1195,11 @@ impl RevoraRevenueShare { (holder.clone(), share_bps), ); // Versioned v2 event: [2, holder, share_bps] — always emitted (#RC26Q2-C31) - Self::emit_v2_event(env, (EVENT_SHARE_SET_V2, issuer, namespace, token), (holder, share_bps)); + Self::emit_v2_event( + env, + (EVENT_SHARE_SET_V2, issuer, namespace, token), + (holder, share_bps), + ); Ok(()) } @@ -4718,7 +4724,9 @@ impl RevoraRevenueShare { env.storage().persistent().set(&entry_key, &entry); // Persist updated per-offering running total. - env.storage().persistent().set(&DataKey::HolderShareTotal(offering_id.clone()), ¤t_total); + env.storage() + .persistent() + .set(&DataKey::HolderShareTotal(offering_id.clone()), ¤t_total); env.events().publish( (EVENT_SNAP_SHARES_APPLIED, issuer, namespace, token), @@ -4775,7 +4783,11 @@ impl RevoraRevenueShare { Self::require_not_frozen(&env)?; Self::require_not_paused(&env)?; issuer.require_auth(); - let offering_id = OfferingId { issuer: issuer.clone(), namespace: namespace.clone(), token: token.clone() }; + let offering_id = OfferingId { + issuer: issuer.clone(), + namespace: namespace.clone(), + token: token.clone(), + }; Self::get_current_issuer( &env, issuer.clone(), @@ -6509,15 +6521,7 @@ mod issue_414_supply_cap_tests { fn setup_with_payment_token( mint_amount: i128, - ) -> ( - Env, - Address, - RevoraRevenueShareClient<'static>, - Address, - Symbol, - Address, - Address, - ) { + ) -> (Env, Address, RevoraRevenueShareClient<'static>, Address, Symbol, Address, Address) { let env = Env::default(); env.mock_all_auths(); @@ -6529,11 +6533,11 @@ mod issue_414_supply_cap_tests { let token_addr = Address::generate(&env); let token_admin = Address::generate(&env); - let payment_token = env.register_stellar_asset_contract(token_admin.clone()); - let payment_token_admin = token::StellarAssetClient::new(&env, &payment_token); + let payment_token = env.register_stellar_asset_contract_v2(token_admin.clone()); + let payment_token_admin = token::StellarAssetClient::new(&env, &payment_token.address()); payment_token_admin.mint(&issuer, &mint_amount); - (env, contract_id, client, issuer, namespace, token_addr, payment_token) + (env, contract_id, client, issuer, namespace, token_addr, payment_token.address()) } #[test] @@ -6542,25 +6546,49 @@ mod issue_414_supply_cap_tests { setup_with_payment_token(2_000_000); assert_eq!( - client - .try_register_offering(&issuer, &namespace, &token_addr, &1_000, &payment_token, &0), + client.try_register_offering( + &issuer, + &namespace, + &token_addr, + &1_000, + &payment_token, + &0 + ), Ok(Ok(())) ); assert_eq!(client.get_supply_cap(&issuer, &namespace, &token_addr), 0); assert_eq!( - client - .try_deposit_revenue(&issuer, &namespace, &token_addr, &payment_token, &700_000, &1), + client.try_deposit_revenue( + &issuer, + &namespace, + &token_addr, + &payment_token, + &700_000, + &1 + ), Ok(Ok(())) ); assert_eq!( - client - .try_deposit_revenue(&issuer, &namespace, &token_addr, &payment_token, &700_000, &2), + client.try_deposit_revenue( + &issuer, + &namespace, + &token_addr, + &payment_token, + &700_000, + &2 + ), Ok(Ok(())) ); assert_eq!( - client - .try_deposit_revenue(&issuer, &namespace, &token_addr, &payment_token, &600_000, &3), + client.try_deposit_revenue( + &issuer, + &namespace, + &token_addr, + &payment_token, + &600_000, + &3 + ), Ok(Ok(())) ); } @@ -6571,8 +6599,14 @@ mod issue_414_supply_cap_tests { setup_with_payment_token(10); assert_eq!( - client - .try_register_offering(&issuer, &namespace, &token_addr, &1_000, &payment_token, &1), + client.try_register_offering( + &issuer, + &namespace, + &token_addr, + &1_000, + &payment_token, + &1 + ), Ok(Ok(())) ); assert_eq!(client.get_supply_cap(&issuer, &namespace, &token_addr), 1); diff --git a/src/test_multisig_gas.rs b/src/test_multisig_gas.rs index dda9ec50..494683d5 100644 --- a/src/test_multisig_gas.rs +++ b/src/test_multisig_gas.rs @@ -114,7 +114,9 @@ fn propose_and_approve( // owners[0] proposes (counts as first approval automatically). let proposer = owners.get(0).unwrap(); let proposal_id = env - .as_contract(contract_id, || RevoraRevenueShare::propose_action(env.clone(), proposer, action)) + .as_contract(contract_id, || { + RevoraRevenueShare::propose_action(env.clone(), proposer, action) + }) .unwrap(); // Collect remaining approvals up to threshold. @@ -145,7 +147,7 @@ fn execute_remove_owner_at_max_owners_within_budget() { let (env, id, _client, _admin, owners) = setup_max_multisig(); let threshold = RevoraRevenueShare::MAX_MULTISIG_OWNERS / 2 + 1; // 11 - // Remove the last owner (index 19) — it is not the proposer. + // Remove the last owner (index 19) — it is not the proposer. let target = owners.get(RevoraRevenueShare::MAX_MULTISIG_OWNERS - 1).unwrap(); let action = ProposalAction::RemoveOwner(target); @@ -225,8 +227,9 @@ fn execute_add_owner_at_max_returns_limit_reached() { let proposal_id = propose_and_approve(&env, &id, &owners, threshold, action); let executor = owners.get(0).unwrap(); - let result = - env.as_contract(&id, || RevoraRevenueShare::execute_action(env.clone(), executor, proposal_id)); + let result = env.as_contract(&id, || { + RevoraRevenueShare::execute_action(env.clone(), executor, proposal_id) + }); assert_eq!( result, @@ -280,12 +283,18 @@ fn execute_remove_owner_below_threshold_returns_limit_reached() { // All 3 must approve to meet threshold = 3. let action = ProposalAction::RemoveOwner(owner3.clone()); let proposal_id = env - .as_contract(&id, || RevoraRevenueShare::propose_action(env.clone(), owner1.clone(), action)) - .unwrap(); - env.as_contract(&id, || RevoraRevenueShare::approve_action(env.clone(), owner2.clone(), proposal_id)) - .unwrap(); - env.as_contract(&id, || RevoraRevenueShare::approve_action(env.clone(), owner3.clone(), proposal_id)) + .as_contract(&id, || { + RevoraRevenueShare::propose_action(env.clone(), owner1.clone(), action) + }) .unwrap(); + env.as_contract(&id, || { + RevoraRevenueShare::approve_action(env.clone(), owner2.clone(), proposal_id) + }) + .unwrap(); + env.as_contract(&id, || { + RevoraRevenueShare::approve_action(env.clone(), owner3.clone(), proposal_id) + }) + .unwrap(); let result = env.as_contract(&id, || { RevoraRevenueShare::execute_action(env.clone(), owner1.clone(), proposal_id) @@ -324,8 +333,9 @@ fn execute_action_non_owner_returns_not_authorized() { // outsider is not in the owners list let outsider = Address::generate(&env); - let result = - env.as_contract(&id, || RevoraRevenueShare::execute_action(env.clone(), outsider, proposal_id)); + let result = env.as_contract(&id, || { + RevoraRevenueShare::execute_action(env.clone(), outsider, proposal_id) + }); assert_eq!( result, @@ -355,8 +365,9 @@ fn execute_action_expired_proposal_returns_proposal_expired() { }); let executor = owners.get(0).unwrap(); - let result = - env.as_contract(&id, || RevoraRevenueShare::execute_action(env.clone(), executor, proposal_id)); + let result = env.as_contract(&id, || { + RevoraRevenueShare::execute_action(env.clone(), executor, proposal_id) + }); assert_eq!( result, @@ -388,8 +399,9 @@ fn execute_action_already_executed_returns_limit_reached() { .unwrap(); // Second execution must fail. - let result = - env.as_contract(&id, || RevoraRevenueShare::execute_action(env.clone(), executor, proposal_id)); + let result = env.as_contract(&id, || { + RevoraRevenueShare::execute_action(env.clone(), executor, proposal_id) + }); assert_eq!( result, Err(RevoraError::LimitReached), diff --git a/src/test_pause_tiers.rs b/src/test_pause_tiers.rs index 53835702..8efa814b 100644 --- a/src/test_pause_tiers.rs +++ b/src/test_pause_tiers.rs @@ -30,13 +30,13 @@ use crate::{PauseState, RevoraError, RevoraRevenueShare, RevoraRevenueShareClien // ── helpers ────────────────────────────────────────────────────────────────── -fn make_client(env: &Env) -> RevoraRevenueShareClient { +fn make_client(env: &Env) -> RevoraRevenueShareClient<'_> { let id = env.register_contract(None, RevoraRevenueShare); RevoraRevenueShareClient::new(env, &id) } /// Initialize with both admin and safety roles; mock all auths for the test. -fn setup(env: &Env) -> (RevoraRevenueShareClient, Address, Address) { +fn setup(env: &Env) -> (RevoraRevenueShareClient<'_>, Address, Address) { env.mock_all_auths(); let client = make_client(env); let admin = Address::generate(env); @@ -51,7 +51,7 @@ fn setup(env: &Env) -> (RevoraRevenueShareClient, Address, Address) { /// Returns `(client, admin, safety, issuer, offering_token, payment_token, holder)`. fn setup_with_offering( env: &Env, -) -> (RevoraRevenueShareClient, Address, Address, Address, Address, Address, Address) { +) -> (RevoraRevenueShareClient<'_>, Address, Address, Address, Address, Address, Address) { env.mock_all_auths(); let client = make_client(env); let admin = Address::generate(env); diff --git a/src/vesting.rs b/src/vesting.rs index 90fcdeb0..87e0e200 100644 --- a/src/vesting.rs +++ b/src/vesting.rs @@ -220,30 +220,34 @@ pub fn migrate_offering_schedules( let mut beneficiaries: Vec
= Vec::new(env); for i in 0..count { - if let Some(beneficiary) = - env.storage().persistent().get(&VestingKey::OfferingScheduleItem(offering_id.clone(), i)) + if let Some(beneficiary) = env + .storage() + .persistent() + .get(&VestingKey::OfferingScheduleItem(offering_id.clone(), i)) { beneficiaries.push_back(beneficiary); } } - let new_offering_id = VestingOfferingId { issuer: new_issuer.clone(), token: offering_id.token.clone() }; + let new_offering_id = + VestingOfferingId { issuer: new_issuer.clone(), token: offering_id.token.clone() }; let mut new_count: u32 = env .storage() .persistent() .get(&VestingKey::OfferingScheduleCount(new_offering_id.clone())) .unwrap_or(0); - let mut migrated = Vec::new(&env); + let mut migrated = Vec::new(env); // First pass: validate that no schedule is pre-cliff. for beneficiary in beneficiaries.iter() { let schedule: Option = env.storage().persistent().get(&VestingKey::Schedule(beneficiary.clone())); if let Some(schedule) = schedule { - if schedule.issuer == offering_id.issuer && schedule.token == offering_id.token { - if now < schedule.cliff_ts { - return Err(VestingError::SchedulePreCliff); - } + if schedule.issuer == offering_id.issuer + && schedule.token == offering_id.token + && now < schedule.cliff_ts + { + return Err(VestingError::SchedulePreCliff); } } } @@ -255,7 +259,9 @@ pub fn migrate_offering_schedules( if let Some(mut schedule) = schedule { if schedule.issuer == offering_id.issuer && schedule.token == offering_id.token { schedule.issuer = new_issuer.clone(); - env.storage().persistent().set(&VestingKey::Schedule(beneficiary.clone()), &schedule); + env.storage() + .persistent() + .set(&VestingKey::Schedule(beneficiary.clone()), &schedule); env.storage().persistent().set( &VestingKey::OfferingScheduleItem(new_offering_id.clone(), new_count), &beneficiary.clone(),