Skip to content
Open
42 changes: 38 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ const EVENT_UNPAUSED: Symbol = symbol_short!("unpaused");
const EVENT_ISSUER_TRANSFER_PROPOSED: Symbol = symbol_short!("iss_prop");
const EVENT_ISSUER_TRANSFER_ACCEPTED: Symbol = symbol_short!("iss_acc");
const EVENT_ISSUER_TRANSFER_CANCELLED: Symbol = symbol_short!("iss_canc");
const EVENT_ISSUER_TRANSFER_REJECTED: Symbol = symbol_short!("iss_rej");
const EVENT_TESTNET_MODE: Symbol = symbol_short!("test_mode");

const EVENT_DIST_CALC: Symbol = symbol_short!("dist_calc");
Expand Down Expand Up @@ -1829,6 +1830,42 @@ impl RevoraRevenueShare {
Ok(())
}

pub fn reject_issuer_transfer(
env: Env,
new_issuer: Address,
namespace: Symbol,
token: Address,
) -> Result<(), RevoraError> {
Self::require_not_frozen(&env)?;
Self::require_not_paused(&env)?;
new_issuer.require_auth();

let offering_id =
Self::find_pending_transfer_for_new_issuer(&env, &namespace, &token, &new_issuer)
.ok_or(RevoraError::NoTransferPending)?;

let _pending: PendingTransfer = env
.storage()
.persistent()
.get(&DataKey::PendingIssuerTransfer(offering_id.clone()))
.ok_or(RevoraError::NoTransferPending)?;

let old_issuer = offering_id.issuer.clone();

env.storage().persistent().remove(&DataKey::PendingIssuerTransfer(offering_id.clone()));

env.events().publish(
(
EVENT_ISSUER_TRANSFER_REJECTED,
offering_id.issuer.clone(),
offering_id.namespace.clone(),
offering_id.token.clone(),
),
(old_issuer, new_issuer.clone()),
);
Ok(())
}

/// Initialize admin and optional safety role for emergency pause (#7).
/// `event_only` configures the contract to skip persistent business state (#72).
/// Can only be called once; panics if already initialized.
Expand Down Expand Up @@ -3265,10 +3302,7 @@ impl RevoraRevenueShare {
/// The maximum allowed blacklist size for the offering.
fn get_effective_blacklist_limit(env: &Env, offering_id: &OfferingId) -> u32 {
let key = DataKey::BlacklistSizeLimit(offering_id.clone());
env.storage()
.persistent()
.get::<DataKey, u32>(&key)
.unwrap_or(MAX_BLACKLIST_SIZE)
env.storage().persistent().get::<DataKey, u32>(&key).unwrap_or(MAX_BLACKLIST_SIZE)
}

/// Set the per-offering blacklist size limit.
Expand Down
76 changes: 76 additions & 0 deletions src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5702,6 +5702,82 @@ fn issuer_transfer_blocked_when_frozen() {
assert!(result.is_err());
}

#[test]
fn issuer_transfer_reject_clears_pending() {
let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup();
let new_issuer = Address::generate(&env);

client.propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer);
client.reject_issuer_transfer(&new_issuer, &symbol_short!("def"), &token);

assert_eq!(client.get_pending_issuer_transfer(&issuer, &symbol_short!("def"), &token), None);
}

#[test]
fn issuer_transfer_reject_emits_event() {
let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup();
let new_issuer = Address::generate(&env);

client.propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer);
let before = legacy_events(&env).len();
client.reject_issuer_transfer(&new_issuer, &symbol_short!("def"), &token);
let after = legacy_events(&env).len();
assert_eq!(after, before + 1);
}

#[test]
fn issuer_transfer_wrong_address_cannot_reject() {
let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup();
let new_issuer = Address::generate(&env);
let wrong_address = Address::generate(&env);

client.propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer);

let result = client.try_reject_issuer_transfer(&wrong_address, &symbol_short!("def"), &token);
assert!(result.is_err());
}

#[test]
fn issuer_transfer_reject_fails_when_no_pending() {
let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup();
let new_issuer = Address::generate(&env);

let result = client.try_reject_issuer_transfer(&new_issuer, &symbol_short!("def"), &token);
assert!(result.is_err());
}

#[test]
fn issuer_transfer_reject_then_can_propose_again() {
let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup();
let new_issuer_1 = Address::generate(&env);
let new_issuer_2 = Address::generate(&env);

client.propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer_1);
client.reject_issuer_transfer(&new_issuer_1, &symbol_short!("def"), &token);

// Should be able to propose to different address
let result =
client.try_propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer_2);
assert!(result.is_ok());
assert_eq!(
client.get_pending_issuer_transfer(&issuer, &symbol_short!("def"), &token),
Some(new_issuer_2)
);
}

#[test]
#[ignore = "legacy host-panic auth test; Soroban aborts process in unit tests"]
fn issuer_transfer_reject_requires_auth() {
let env = Env::default();
let contract_id = env.register_contract(None, RevoraRevenueShare);
let client = RevoraRevenueShareClient::new(&env, &contract_id);
let new_issuer = Address::generate(&env);
let token = Address::generate(&env);

// No mock_all_auths - should panic
client.reject_issuer_transfer(&new_issuer, &symbol_short!("def"), &token);
}

// ===========================================================================
// Multisig admin pattern tests
// ===========================================================================
Expand Down
5 changes: 1 addition & 4 deletions src/test_min_revenue_threshold_boundary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,7 @@ fn override_existing_below_threshold_bypasses_check() {
let s = client.get_audit_summary(&issuer, &symbol_short!("def"), &token).unwrap();
assert_eq!(s.total_revenue, 300, "audit must reflect corrected amount");
assert_eq!(s.report_count, 1, "report_count must not change on override");
assert_eq!(
client.get_revenue_by_period(&issuer, &symbol_short!("def"), &token, &1),
300
);
assert_eq!(client.get_revenue_by_period(&issuer, &symbol_short!("def"), &token, &1), 300);
}

/// get_min_revenue_threshold returns the stored value and updates correctly.
Expand Down
Loading