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
14 changes: 6 additions & 8 deletions .github/workflows/invariant-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ env:
RUST_VERSION: '1.85'
# Number of proptest cases per property. Increase for deeper fuzzing.
PROPTEST_CASES: 200
PROPTEST_MAX_SHRINK_ITERS: 10000

jobs:
# ─────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -42,18 +43,14 @@ jobs:
working-directory: ./contracts
env:
PROPTEST_CASES: ${{ env.PROPTEST_CASES }}
PROPTEST_MAX_SHRINK_ITERS: ${{ env.PROPTEST_MAX_SHRINK_ITERS }}
run: |
if cargo test --test invariants --no-run >/dev/null 2>&1; then
cargo test --test invariants -- --nocapture 2>&1 | tee invariant-test-results.txt
else
echo "::warning::Cargo test target 'invariants' is not registered; running the full contract suite instead." | tee invariant-test-results.txt
cargo test --verbose 2>&1 | tee -a invariant-test-results.txt
fi
cargo test -p subtrackr-subscription --test property_invariants -- --nocapture 2>&1 | tee invariant-test-results.txt

# ── Run all contract tests to ensure nothing regressed ─────────────
- name: Run full contract test suite
working-directory: ./contracts
run: cargo test --verbose
run: cargo test --workspace --lib --verbose

# ── Upload test results as artifact ───────────────────────────────
- name: Upload invariant test results
Expand Down Expand Up @@ -90,8 +87,9 @@ jobs:
working-directory: ./contracts
env:
PROPTEST_CASES: 1000
PROPTEST_MAX_SHRINK_ITERS: ${{ env.PROPTEST_MAX_SHRINK_ITERS }}
run: |
cargo test --test invariants -- --nocapture 2>&1 | tee extended-fuzz-results.txt
cargo test -p subtrackr-subscription --test property_invariants -- --nocapture 2>&1 | tee extended-fuzz-results.txt

- name: Upload extended fuzz results
if: always()
Expand Down
51 changes: 27 additions & 24 deletions contracts/fraud/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,17 @@ fn get_merchant_subscriptions(env: &Env, merchant: &Address) -> Vec<Subscription
}

fn save_subscriptions(env: &Env, subscriber: &Address, ids: &Vec<SubscriptionId>) {
env.storage()
.persistent()
.set(&StorageKey::SubscriberSubscriptions(subscriber.clone()), &ids.clone());
env.storage().persistent().set(
&StorageKey::SubscriberSubscriptions(subscriber.clone()),
&ids.clone(),
);
}

fn save_merchant_subscriptions(env: &Env, merchant: &Address, ids: &Vec<SubscriptionId>) {
env.storage()
.persistent()
.set(&StorageKey::MerchantSubscriptions(merchant.clone()), &ids.clone());
env.storage().persistent().set(
&StorageKey::MerchantSubscriptions(merchant.clone()),
&ids.clone(),
);
}

fn load_profile(env: &Env, subscription_id: SubscriptionId) -> Option<SubscriptionProfile> {
Expand All @@ -91,12 +93,17 @@ fn load_profile(env: &Env, subscription_id: SubscriptionId) -> Option<Subscripti
}

fn save_profile(env: &Env, profile: &SubscriptionProfile) {
env.storage()
.persistent()
.set(&StorageKey::Subscription(profile.subscription_id), &profile.clone());
env.storage().persistent().set(
&StorageKey::Subscription(profile.subscription_id),
&profile.clone(),
);
}

fn determine_velocity_score(env: &Env, profile: &SubscriptionProfile, ids: &Vec<SubscriptionId>) -> u32 {
fn determine_velocity_score(
env: &Env,
profile: &SubscriptionProfile,
ids: &Vec<SubscriptionId>,
) -> u32 {
let now = env.ledger().timestamp();
let mut recent_creations = 0u32;
let mut i = 0u32;
Expand Down Expand Up @@ -159,11 +166,7 @@ fn determine_action(score: u32) -> FraudAction {
}
}

fn score_profile(
env: &Env,
profile: &SubscriptionProfile,
ids: &Vec<SubscriptionId>,
) -> RiskScore {
fn score_profile(env: &Env, profile: &SubscriptionProfile, ids: &Vec<SubscriptionId>) -> RiskScore {
let now = env.ledger().timestamp();
let velocity_score = determine_velocity_score(env, profile, ids);
let anomaly_score = determine_anomaly_score(profile);
Expand Down Expand Up @@ -233,9 +236,10 @@ fn persist_case(env: &Env, score: &RiskScore, status: FraudReviewStatus) -> Frau
updated_at: score.assessed_at,
};

env.storage()
.persistent()
.set(&StorageKey::ReviewCase(score.subscription_id), &case.clone());
env.storage().persistent().set(
&StorageKey::ReviewCase(score.subscription_id),
&case.clone(),
);
case
}

Expand Down Expand Up @@ -309,11 +313,7 @@ impl SubTrackrFraud {
}
}

pub fn record_chargeback(
env: Env,
subscriber: Address,
subscription_id: SubscriptionId,
) {
pub fn record_chargeback(env: Env, subscriber: Address, subscription_id: SubscriptionId) {
subscriber.require_auth();

if let Some(mut profile) = load_profile(&env, subscription_id) {
Expand Down Expand Up @@ -373,7 +373,10 @@ impl SubTrackrFraud {
let case = persist_case(&env, &score, status);
update_profile_action(&env, subscription_id, &score);
env.events().publish(
(String::from_str(&env, "fraud_case_opened"), score.subscription_id),
(
String::from_str(&env, "fraud_case_opened"),
score.subscription_id,
),
(case.risk_score, case.action.clone()),
);
} else {
Expand Down
2 changes: 2 additions & 0 deletions contracts/subscription/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ subtrackr-types = { path = "../types" }

[dev-dependencies]
soroban-sdk = { version = "21.0.0", features = ["testutils"] }
subtrackr-storage = { path = "../storage" }
proptest = "1.4.0"
83 changes: 61 additions & 22 deletions contracts/subscription/src/gas_optimization.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/// Gas Optimization and Targeting Module
/// Provides optimization recommendations and tracks gas targets

use soroban_sdk::{String, Vec, Env};
use soroban_sdk::{Env, String, Vec};

/// Optimization level
#[derive(Clone, Copy)]
Expand Down Expand Up @@ -112,20 +111,56 @@ impl GasOptimizationTargets {
pub fn all_targets(env: &Env) -> Vec<(String, u64)> {
soroban_sdk::vec![
env,
(String::from_str(env, "initialize"), Self::initialize_target()),
(String::from_str(env, "create_plan"), Self::create_plan_target()),
(
String::from_str(env, "initialize"),
Self::initialize_target()
),
(
String::from_str(env, "create_plan"),
Self::create_plan_target()
),
(String::from_str(env, "subscribe"), Self::subscribe_target()),
(String::from_str(env, "charge_subscription"), Self::charge_subscription_target()),
(String::from_str(env, "cancel_subscription"), Self::cancel_subscription_target()),
(String::from_str(env, "pause_subscription"), Self::pause_subscription_target()),
(String::from_str(env, "resume_subscription"), Self::resume_subscription_target()),
(String::from_str(env, "request_refund"), Self::request_refund_target()),
(String::from_str(env, "approve_refund"), Self::approve_refund_target()),
(String::from_str(env, "request_transfer"), Self::request_transfer_target()),
(String::from_str(env, "accept_transfer"), Self::accept_transfer_target()),
(
String::from_str(env, "charge_subscription"),
Self::charge_subscription_target()
),
(
String::from_str(env, "cancel_subscription"),
Self::cancel_subscription_target()
),
(
String::from_str(env, "pause_subscription"),
Self::pause_subscription_target()
),
(
String::from_str(env, "resume_subscription"),
Self::resume_subscription_target()
),
(
String::from_str(env, "request_refund"),
Self::request_refund_target()
),
(
String::from_str(env, "approve_refund"),
Self::approve_refund_target()
),
(
String::from_str(env, "request_transfer"),
Self::request_transfer_target()
),
(
String::from_str(env, "accept_transfer"),
Self::accept_transfer_target()
),
(String::from_str(env, "get_plan"), Self::get_plan_target()),
(String::from_str(env, "get_subscription"), Self::get_subscription_target()),
(String::from_str(env, "get_user_subscriptions"), Self::get_user_subscriptions_target()),
(
String::from_str(env, "get_subscription"),
Self::get_subscription_target()
),
(
String::from_str(env, "get_user_subscriptions"),
Self::get_user_subscriptions_target()
),
]
}
}
Expand All @@ -135,7 +170,11 @@ pub struct GasOptimizations;

impl GasOptimizations {
/// Get optimization recommendations for a specific function
pub fn get_recommendations_for_function(env: &Env, function_name: &str, current_gas: u64) -> Vec<String> {
pub fn get_recommendations_for_function(
env: &Env,
function_name: &str,
current_gas: u64,
) -> Vec<String> {
let mut recommendations = Vec::new(env);

match function_name {
Expand Down Expand Up @@ -192,7 +231,10 @@ impl GasOptimizations {
}
}
_ => {
recommendations.push_back(String::from_str(env, "Monitor function for optimization opportunities"));
recommendations.push_back(String::from_str(
env,
"Monitor function for optimization opportunities",
));
}
}

Expand Down Expand Up @@ -270,7 +312,7 @@ impl GasOptimizations {
}
}

pub fn get_optimization_priorities(
pub fn get_optimization_priorities(
env: &Env,
gas_metrics: Vec<(String, u64)>,
) -> Vec<(String, u64, String)> {
Expand All @@ -279,7 +321,7 @@ impl GasOptimizations {

/// Best practices for gas efficiency
pub mod best_practices {
use soroban_sdk::{String, Vec, Env};
use soroban_sdk::{Env, String, Vec};

pub fn get_storage_best_practices(env: &Env) -> Vec<String> {
let mut practices = Vec::new(env);
Expand Down Expand Up @@ -353,10 +395,7 @@ pub mod best_practices {
pub fn get_validation_best_practices(env: &Env) -> Vec<String> {
let mut practices = Vec::new(env);

practices.push_back(String::from_str(
env,
"Validate inputs early to fail fast",
));
practices.push_back(String::from_str(env, "Validate inputs early to fail fast"));
practices.push_back(String::from_str(
env,
"Use assertions for critical validations",
Expand Down
Loading