Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 48 additions & 14 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,8 @@ const EVENT_ROUNDING_MODE_SET: Symbol = symbol_short!("rnd_mode");
const EVENT_ADMIN_SET: Symbol = symbol_short!("admin_set");
const EVENT_PLATFORM_FEE_SET: Symbol = symbol_short!("fee_set");
const BPS_DENOMINATOR: i128 = 10_000;
/// E18 fixed-point precision.
const E18: i128 = 1_000_000_000_000_000_000;
/// Stellar network canonical decimal precision (7 decimal places, i.e., stroops).
const STELLAR_CANONICAL_DECIMALS: u32 = 7;
/// Maximum accepted decimal precision (safety cap for normalization math).
Expand Down Expand Up @@ -649,8 +651,12 @@ pub enum DataKey {
PeriodEntry(OfferingId, u32),
/// Total number of deposited periods for an offering.
PeriodCount(OfferingId),
/// Per-offering accrual index in e18 fixed-point.
AccrualIndexE18(OfferingId),
/// Holder's share in basis points for (offering_id, holder).
HolderShare(OfferingId, Address),
/// Last accrual index a holder has claimed up to.
LastClaimedAccrualIndex(OfferingId, Address),
/// Per-offering running total of all persisted holder shares (basis points).
HolderShareTotal(OfferingId),
/// Next period index to claim for (offering_id, holder).
Expand Down Expand Up @@ -767,6 +773,11 @@ pub enum DataKey2 {

/// Sealed-period flag: when present, `report_revenue` overrides are rejected for this period.
ClosedPeriod(OfferingId, u64),

/// Per-offering supply cap (#96).
SupplyCap(OfferingId),
/// Per-offering total deposited revenue (#96).
DepositedRevenue(OfferingId),
}

/// Maximum number of offerings returned in a single page.
Expand Down Expand Up @@ -1251,11 +1262,17 @@ impl RevoraRevenueShare {
return Err(RevoraError::InvalidShareBps);
}

// Persist updated holder share and running total.
// Persist updated holder share, running total, and update last claimed accrual index.
env.storage()
.persistent()
.set(&DataKey::HolderShare(offering_id.clone(), holder.clone()), &share_bps);
env.storage().persistent().set(&total_key, &new_total);

// Update last claimed accrual index to current index to settle existing accrual
let current_accrual_key = DataKey::AccrualIndexE18(offering_id.clone());
let current_accrual: i128 = env.storage().persistent().get(&current_accrual_key).unwrap_or(0);
let last_claimed_accrual_key = DataKey::LastClaimedAccrualIndex(offering_id.clone(), holder.clone());
env.storage().persistent().set(&last_claimed_accrual_key, &current_accrual);

env.events().publish(
(EVENT_SHARE_SET, issuer.clone(), namespace.clone(), token.clone()),
Expand Down Expand Up @@ -1399,6 +1416,22 @@ impl RevoraRevenueShare {
);
}

// Update the e18 accrual index
let decimals = Self::get_payment_token_decimals(env.clone(), issuer.clone(), namespace.clone(), token.clone());
let normalized_amount = Self::normalize_amount(amount, decimals);
let total_share_bps_key = DataKey::HolderShareTotal(offering_id.clone());
let total_share_bps: u32 = env.storage().persistent().get(&total_share_bps_key).unwrap_or(0);

if total_share_bps > 0 {
let accrual_delta = (normalized_amount.checked_mul(E18))
.and_then(|x| x.checked_div(total_share_bps as i128))
.unwrap_or(0);
let current_accrual_key = DataKey::AccrualIndexE18(offering_id.clone());
let current_accrual: i128 = env.storage().persistent().get(&current_accrual_key).unwrap_or(0);
let new_accrual = current_accrual.checked_add(accrual_delta).unwrap_or(current_accrual);
env.storage().persistent().set(&current_accrual_key, &new_accrual);
}

// Versioned event v2: [version: u32, payment_token: Address, amount: i128, period_id: u64]
Self::emit_v2_event(
env,
Expand Down Expand Up @@ -5372,7 +5405,6 @@ impl RevoraRevenueShare {
let delay_secs: u64 = env.storage().persistent().get(&delay_key).unwrap_or(0);
let now = env.ledger().timestamp();

let mut total_payout: i128 = 0;
let mut claimed_periods = Vec::new(&env);
let mut last_claimed_idx = start_idx;
let mut previous_period_id: Option<u64> = None;
Expand Down Expand Up @@ -5410,17 +5442,6 @@ impl RevoraRevenueShare {
if delay_secs > 0 && now < deposit_time.saturating_add(delay_secs) {
break;
}
let rev_key = DataKey::PeriodRevenue(offering_id.clone(), period_id);
let revenue: i128 = env.storage().persistent().get(&rev_key).unwrap();
let decimals = Self::get_payment_token_decimals(
env.clone(),
offering_id.issuer.clone(),
offering_id.namespace.clone(),
offering_id.token.clone(),
);
let normalized = Self::normalize_amount(revenue, decimals);
let payout = normalized * (share_bps as i128) / 10_000;
total_payout += payout;
claimed_periods.push_back(period_id);
last_claimed_idx = i + 1;
}
Expand All @@ -5429,6 +5450,18 @@ impl RevoraRevenueShare {
return Err(RevoraError::ClaimDelayNotElapsed);
}

// Calculate total payout using e18 accrual index
let current_accrual_key = DataKey::AccrualIndexE18(offering_id.clone());
let current_accrual: i128 = env.storage().persistent().get(&current_accrual_key).unwrap_or(0);
let last_claimed_accrual_key = DataKey::LastClaimedAccrualIndex(offering_id.clone(), holder.clone());
let last_claimed_accrual: i128 = env.storage().persistent().get(&last_claimed_accrual_key).unwrap_or(0);
let accrual_delta = current_accrual - last_claimed_accrual;

let total_payout = (accrual_delta.checked_mul(share_bps as i128))
.and_then(|x| x.checked_div(BPS_DENOMINATOR))
.and_then(|x| x.checked_div(E18))
.unwrap_or(0);

// Transfer only if there is a positive payout
if total_payout > 0 {
let payment_token = Self::get_locked_payment_token_for_offering(&env, &offering_id)
Expand All @@ -5442,8 +5475,9 @@ impl RevoraRevenueShare {
}
}

// Advance claim index only for periods actually claimed (respecting delay)
// Advance claim indices only for periods actually claimed (respecting delay)
env.storage().persistent().set(&idx_key, &last_claimed_idx);
env.storage().persistent().set(&last_claimed_accrual_key, &current_accrual);

// Versioned v2 event: [2, holder, total_payout, periods] ΓÇö always emitted (#RC26Q2-C31)
Self::emit_v2_event(
Expand Down
Loading