Skip to content
Open
Show file tree
Hide file tree
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
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ edition = "2021"
# Issue #465: Kani bounded verification harness (not enabled in default CI).
kani = []

# `kani` is a cfg set by the Kani verifier (not a Cargo feature), so declare it as a
# known cfg to keep `unexpected_cfgs` quiet under `clippy --all-features -D warnings`.
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(kani)'] }

[package.metadata.kani]
unwind = 4

Expand Down
349 changes: 255 additions & 94 deletions src/lib.rs

Large diffs are not rendered by default.

19 changes: 11 additions & 8 deletions src/test_claim_transfer_fail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@

use crate::{RevoraError, RevoraRevenueShare, RevoraRevenueShareClient};
use soroban_sdk::{
contract, contractimpl, contracttype, symbol_short, testutils::Address as _, token, Address,
Env, String,
contract, contractimpl, contracttype, symbol_short, testutils::Address as _, Address, Env,
String,
};

// ══════════════════════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -181,13 +181,16 @@ fn pending_periods(
holder: &Address,
) -> soroban_sdk::Vec<u64> {
env.as_contract(revora_id, || {
RevoraRevenueShare::get_pending_periods(
RevoraRevenueShare::get_pending_periods_page(
env.clone(),
issuer.clone(),
symbol_short!("def"),
offering_token.clone(),
holder.clone(),
0,
50,
)
.0
})
}

Expand Down Expand Up @@ -435,25 +438,25 @@ fn claim_transfer_fail_does_not_affect_sibling_offering() {
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
// Register a second offering backed by a normal (unarmed) token so its claim succeeds.
let offering_token_b = Address::generate(&env);
let admin_b = Address::generate(&env);

let (token_b_id, token_b) = deploy_failing_token(&env);
token_b.mint(&issuer, &1_000_000);

revora.register_offering(
&issuer,
&symbol_short!("def"),
&offering_token_b,
&10_000,

&token_b_id,
&0,
);
revora.set_holder_share(&issuer, &symbol_short!("def"), &offering_token_b, &holder, &10_000);
revora.deposit_revenue(
&issuer,
&symbol_short!("def"),
&offering_token_b,

&token_b_id,
&100_000,
&1,
);
Expand Down
17 changes: 8 additions & 9 deletions src/test_close_period.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@
use super::*;
use soroban_sdk::{
testutils::{Address as _, Events as _, Ledger},
token,
Address, Env,
token, Address, Env,
};

// ── Helpers ──────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -134,8 +133,7 @@ fn override_after_close_returns_period_already_closed() {
client.close_period(&issuer, &ns, &token, &1);

// Attempt override — must be rejected.
let result =
client.try_report_revenue(&issuer, &ns, &token, &payment_token, &2_000, &1, &true);
let result = client.try_report_revenue(&issuer, &ns, &token, &payment_token, &2_000, &1, &true);
assert_eq!(result, Err(Ok(RevoraError::PeriodAlreadyClosed)));
}

Expand All @@ -149,9 +147,11 @@ fn initial_report_for_new_period_after_close_is_allowed() {
client.close_period(&issuer, &ns, &token, &1);

// A brand-new period 2 (initial report, not an override) must still be accepted.
let result =
client.try_report_revenue(&issuer, &ns, &token, &payment_token, &500, &2, &false);
assert!(result.is_ok(), "initial report for a new period should succeed after closing period 1");
let result = client.try_report_revenue(&issuer, &ns, &token, &payment_token, &500, &2, &false);
assert!(
result.is_ok(),
"initial report for a new period should succeed after closing period 1"
);
}

#[test]
Expand Down Expand Up @@ -206,8 +206,7 @@ fn close_period_does_not_affect_other_periods() {
assert!(!client.is_period_closed(&issuer, &ns, &token, &2));

// Override of period 2 must still succeed.
let result =
client.try_report_revenue(&issuer, &ns, &token, &payment_token, &999, &2, &true);
let result = client.try_report_revenue(&issuer, &ns, &token, &payment_token, &999, &2, &true);
assert!(result.is_ok(), "override of an open period must succeed");
}

Expand Down
41 changes: 15 additions & 26 deletions src/test_compute_share_invariants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,11 @@

#![cfg(test)]

extern crate alloc;

use crate::{RevoraRevenueShare, RevoraRevenueShareClient, RoundingMode};
use soroban_sdk::{testutils::Address as _, Address, Env};
use alloc::format;
use soroban_sdk::Env;

// ── Helper ────────────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -143,9 +146,9 @@ fn round_half_up_table_driven() {
(-10_000, 5_000, -5_000),
// 1 bps
(10_000, 1, 1),
(9_999, 1, 1), // 0.9999 rounds up to 1
(4_999, 1, 0), // 0.4999 rounds down
(5_000, 1, 1), // exactly 0.5 rounds up
(9_999, 1, 1), // 0.9999 rounds up to 1
(4_999, 1, 0), // 0.4999 rounds down
(5_000, 1, 1), // exactly 0.5 rounds up
// Over-bps guard
(1_000_000, 10_001, 0),
];
Expand Down Expand Up @@ -314,10 +317,7 @@ fn round_half_up_gte_truncation_for_positive_amounts() {
for &bps in bps_values {
let t = c.compute_share(&amount, &bps, &RoundingMode::Truncation);
let r = c.compute_share(&amount, &bps, &RoundingMode::RoundHalfUp);
assert!(
r >= t,
"RoundHalfUp ({r}) < Truncation ({t}) for amount={amount}, bps={bps}"
);
assert!(r >= t, "RoundHalfUp ({r}) < Truncation ({t}) for amount={amount}, bps={bps}");
assert_bounds(t, amount, &format!("Truncation amount={amount} bps={bps}"));
assert_bounds(r, amount, &format!("RoundHalfUp amount={amount} bps={bps}"));
}
Expand Down Expand Up @@ -428,18 +428,14 @@ fn rounding_boundary_negative_half() {
assert_eq!(c.compute_share(&-3, &5_000, &RoundingMode::RoundHalfUp), -2);
}


// ═══════════════════════════════════════════════════════════════════════════════
// Issue #465: i128::MIN — naive multiply must panic, decomposition must not wrap
// ═══════════════════════════════════════════════════════════════════════════════

#[test]
fn i128_min_naive_multiply_overflow_is_detected() {
// Naive `amount * bps` overflows for i128::MIN at full bps; must not silently wrap.
assert!(
i128::MIN.checked_mul(10_000).is_none(),
"i128::MIN * 10_000 must not fit in i128"
);
assert!(i128::MIN.checked_mul(10_000).is_none(), "i128::MIN * 10_000 must not fit in i128");
}

/// Naive multiply reference — panics instead of silently wrapping on overflow.
Expand All @@ -466,7 +462,6 @@ fn i128_min_full_bps_decomposition_is_exact_not_wrapped() {
assert_bounds(result_round, i128::MIN, "i128::MIN full bps RoundHalfUp");
}


// ═══════════════════════════════════════════════════════════════════════════════
// Issue #373: compute_share RoundHalfUp & Extreme i128 Value Tests
// ═══════════════════════════════════════════════════════════════════════════════
Expand All @@ -478,10 +473,10 @@ fn compute_share_roundhalfup_negative_amount_edge_cases() {

// Test exact half-unit with negative amounts
// For negative amounts, "rounding away from zero" means more negative

// amount = -15000, bps = 5000 → exact -7500 (no rounding needed)
assert_eq!(c.compute_share(&-15000, &5000, &RoundingMode::RoundHalfUp), -7500);

// amount = -15001, bps = 5000 → -7500.5 → should round to -7501 (away from zero)
let result = c.compute_share(&-15001, &5000, &RoundingMode::RoundHalfUp);
assert_eq!(result, -7501, "Negative half should round away from zero");
Expand Down Expand Up @@ -589,8 +584,8 @@ fn remainder_product_bound_holds_for_all_bps() {
20_000,
100_000,
1_000_000,
i128::MAX / 10_000 * 10_000 + 9_999, // Max remainder
i128::MIN / 10_000 * 10_000 - 9_999, // Min remainder
i128::MAX - 9_999, // Near-max extreme amount (non-zero remainder)
i128::MIN + 9_999, // Near-min extreme amount (non-zero remainder)
];

let bps_values = [1_u32, 100, 1_000, 5_000, 9_999, 10_000];
Expand Down Expand Up @@ -628,19 +623,13 @@ fn checked_mul_defense_in_depth_prevents_overflow() {

// Test with extreme values that would be problematic without checked_mul
// The decomposition ensures |r| < 10_000, but we test the saturating fallback path
let extreme_amounts = [
i128::MAX,
i128::MIN,
i128::MAX - 1,
i128::MIN + 1,
];
let extreme_amounts = [i128::MAX, i128::MIN, i128::MAX - 1, i128::MIN + 1];

for &amount in &extreme_amounts {
for &bps in [1_u32, 5_000, 10_000] {
for bps in [1_u32, 5_000, 10_000] {
let result = c.compute_share(&amount, &bps, &RoundingMode::Truncation);
// Should never panic and should always satisfy bounds
assert_bounds(result, amount, &format!("Extreme amount={amount} bps={bps}"));
}
}
}

24 changes: 12 additions & 12 deletions src/test_event_indexed_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ fn event_indexed_v2_rv_init_topic_and_data_shape() {
let before = env.events().all().len();
client.report_revenue(&issuer, &ns, &token, &payout, &10_000, &1, &false);

let (topic, data) = find_indexed_v2(&env, symbol_short!("rv_init"), before as u32)
let (topic, data) = find_indexed_v2(&env, symbol_short!("rv_init"), before)
.expect("rv_init EVENT_INDEXED_V2 must be emitted on initial report");

// Topic shape
Expand Down Expand Up @@ -103,7 +103,7 @@ fn event_indexed_v2_rv_rej_topic_and_data_shape() {
// Same period_id + override_existing=false → rv_rej
client.report_revenue(&issuer, &ns, &token, &payout, &20_000, &1, &false);

let (topic, data) = find_indexed_v2(&env, symbol_short!("rv_rej"), before as u32)
let (topic, data) = find_indexed_v2(&env, symbol_short!("rv_rej"), before)
.expect("rv_rej EVENT_INDEXED_V2 must be emitted on duplicate report");

assert_eq!(topic.version, 2);
Expand Down Expand Up @@ -131,7 +131,7 @@ fn event_indexed_v2_rv_ovr_topic_and_data_shape() {
// override_existing=true → rv_ovr
client.report_revenue(&issuer, &ns, &token, &payout, &15_000, &1, &true);

let (topic, data) = find_indexed_v2(&env, symbol_short!("rv_ovr"), before as u32)
let (topic, data) = find_indexed_v2(&env, symbol_short!("rv_ovr"), before)
.expect("rv_ovr EVENT_INDEXED_V2 must be emitted on override");

assert_eq!(topic.version, 2);
Expand All @@ -157,7 +157,7 @@ fn event_indexed_v2_rv_rep_topic_and_data_shape() {
let before = env.events().all().len();
client.report_revenue(&issuer, &ns, &token, &payout, &10_000, &1, &false);

let (topic, data) = find_indexed_v2(&env, symbol_short!("rv_rep"), before as u32)
let (topic, data) = find_indexed_v2(&env, symbol_short!("rv_rep"), before)
.expect("rv_rep EVENT_INDEXED_V2 must be emitted unconditionally");

assert_eq!(topic.version, 2);
Expand All @@ -182,7 +182,7 @@ fn event_indexed_v2_rv_rep_actual_override_true_on_correction() {
let before = env.events().all().len();
client.report_revenue(&issuer, &ns, &token, &payout, &15_000, &1, &true);

let (_, data) = find_indexed_v2(&env, symbol_short!("rv_rep"), before as u32).unwrap();
let (_, data) = find_indexed_v2(&env, symbol_short!("rv_rep"), before).unwrap();
let (_, _, actual_override): (i128, Address, bool) = data.into_val(&env);
assert!(actual_override);
}
Expand All @@ -201,7 +201,7 @@ fn event_indexed_v2_claim_topic_and_data_shape() {
let issuer = Address::generate(&env);
let ns = symbol_short!("test");
let token = Address::generate(&env);
let payout = env.register_stellar_asset_contract(admin.clone());
let payout = env.register_stellar_asset_contract_v2(admin.clone()).address();
soroban_sdk::token::StellarAssetClient::new(&env, &payout).mint(&issuer, &1_000_000);

client.initialize(&admin, &None::<Address>, &None::<bool>);
Expand All @@ -213,7 +213,7 @@ fn event_indexed_v2_claim_topic_and_data_shape() {
let before = env.events().all().len();
client.claim(&holder, &issuer, &ns, &token, &10);

let (topic, data) = find_indexed_v2(&env, symbol_short!("claim"), before as u32)
let (topic, data) = find_indexed_v2(&env, symbol_short!("claim"), before)
.expect("claim EVENT_INDEXED_V2 must be emitted");

assert_eq!(topic.version, 2);
Expand All @@ -239,7 +239,7 @@ fn event_indexed_v2_claim_period_id_always_zero() {
let issuer = Address::generate(&env);
let ns = symbol_short!("test");
let token = Address::generate(&env);
let payout = env.register_stellar_asset_contract(admin.clone());
let payout = env.register_stellar_asset_contract_v2(admin.clone()).address();
soroban_sdk::token::StellarAssetClient::new(&env, &payout).mint(&issuer, &1_000_000);

client.initialize(&admin, &None::<Address>, &None::<bool>);
Expand All @@ -252,7 +252,7 @@ fn event_indexed_v2_claim_period_id_always_zero() {
let before = env.events().all().len();
client.claim(&holder, &issuer, &ns, &token, &10);

let (topic, _) = find_indexed_v2(&env, symbol_short!("claim"), before as u32).unwrap();
let (topic, _) = find_indexed_v2(&env, symbol_short!("claim"), before).unwrap();
assert_eq!(topic.period_id, 0);
}

Expand All @@ -278,13 +278,13 @@ fn event_indexed_v2_payout_asset_bound_correctly_per_offering() {

let before_a = env.events().all().len();
client.report_revenue(&issuer, &ns, &token_a, &payout_a, &10_000, &1, &false);
let (_, data_a) = find_indexed_v2(&env, symbol_short!("rv_init"), before_a as u32).unwrap();
let (_, data_a) = find_indexed_v2(&env, symbol_short!("rv_init"), before_a).unwrap();
let (_, asset_a): (i128, Address) = data_a.into_val(&env);
assert_eq!(asset_a, payout_a);

let before_b = env.events().all().len();
client.report_revenue(&issuer, &ns, &token_b, &payout_b, &20_000, &1, &false);
let (_, data_b) = find_indexed_v2(&env, symbol_short!("rv_init"), before_b as u32).unwrap();
let (_, data_b) = find_indexed_v2(&env, symbol_short!("rv_init"), before_b).unwrap();
let (_, asset_b): (i128, Address) = data_b.into_val(&env);
assert_eq!(asset_b, payout_b);
}
Expand All @@ -302,7 +302,7 @@ fn event_indexed_v2_version_field_always_2() {
let ev_idx2 = symbol_short!("ev_idx2");
let all = env.events().all();
let mut count = 0u32;
for i in before as u32..all.len() {
for i in before..all.len() {
let (_, topics, _) = all.get(i).unwrap();
if topics.len() >= 2 {
let t0: Symbol = topics.get(0).unwrap().into_val(&env);
Expand Down
Loading
Loading