Skip to content
Closed
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
36 changes: 36 additions & 0 deletions risk_score/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions risk_score/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ crate-type = ["cdylib"]
[dependencies]
soroban-sdk = "22.0.8"

[dev-dependencies]
soroban-sdk = { version = "22.0.8", features = ["testutils"] }


[profile.release]
opt-level = "z"
overflow-checks = true
Expand Down
130 changes: 68 additions & 62 deletions risk_score/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, Map, Symbo

/// Enhanced Risk & Tier Management Contract
/// Stores risk scores with tier classifications and timestamps
///
const DAY_IN_LEDGERS: u32 = 17280;
const BUMP_AMOUNT: u32 = 30 * DAY_IN_LEDGERS;
const LIFETIME_THRESHOLD: u32 = 15 * DAY_IN_LEDGERS;
#[contract]
pub struct RiskTierContract;

Expand Down Expand Up @@ -48,20 +52,35 @@ impl RiskTierContract {
// Use tuple key for better organization: (user, "risk_tier")
let tuple_key = (user.clone(), Symbol::new(&env, "risk_tier"));
env.storage().persistent().set(&tuple_key, &risk_data);
env.storage().persistent().extend_ttl(&tuple_key, LIFETIME_THRESHOLD, BUMP_AMOUNT);

// Also store in tier-based index for efficient queries
let tier_key = (tier.clone(), Symbol::new(&env, "users"));
let mut tier_users: Vec<Address> = env


let user_membership_key = (tier.clone(), Symbol::new(&env, "is_member"), user.clone());
let is_already_member: bool = env
.storage()
.persistent()
.get(&tier_key)
.unwrap_or(Vec::new(&env));
.get(&user_membership_key)
.unwrap_or(false);

// Add user to tier list if not already present
if !tier_users.contains(&user) {
tier_users.push_back(user.clone());
env.storage().persistent().set(&tier_key, &tier_users);

if !is_already_member {

env.storage().persistent().set(&user_membership_key, &true);
env.storage().persistent().extend_ttl(&user_membership_key, LIFETIME_THRESHOLD, BUMP_AMOUNT);


let stats_key = (tier.clone(), Symbol::new(&env, "count"));
let current_count: u32 = env
.storage()
.persistent()
.get(&stats_key)
.unwrap_or(0);

env.storage().persistent().set(&stats_key, &(current_count + 1));
env.storage().persistent().extend_ttl(&stats_key, LIFETIME_THRESHOLD, BUMP_AMOUNT);
}
// --------------------------

// Store user's chosen tier separately for quick access
let chosen_key = (user.clone(), Symbol::new(&env, "chosen_tier"));
Expand All @@ -77,52 +96,51 @@ impl RiskTierContract {
/// Get complete risk and tier data for user
pub fn get_risk_tier(env: Env, user: Address) -> Option<RiskTierData> {
let tuple_key = (user, Symbol::new(&env, "risk_tier"));

if env.storage().persistent().has(&tuple_key) {
env.storage().persistent().extend_ttl(&tuple_key, LIFETIME_THRESHOLD, BUMP_AMOUNT);
}
env.storage().persistent().get(&tuple_key)
}

/// Get only risk score (backward compatibility)
pub fn get_score(env: Env, user: Address) -> u32 {
pub fn get_score(env: Env, user: Address) -> u32 {
let tuple_key = (user, Symbol::new(&env, "risk_tier"));
if let Some(data) = env
.storage()
.persistent()
.get::<_, RiskTierData>(&tuple_key)
{

if let Some(data) = env.storage().persistent().get::<_, RiskTierData>(&tuple_key) {
env.storage().persistent().extend_ttl(&tuple_key, LIFETIME_THRESHOLD, BUMP_AMOUNT);
data.score
} else {
0
}
}

/// Get user's chosen tier for operations
pub fn get_chosen_tier(env: Env, user: Address) -> Symbol {
pub fn get_chosen_tier(env: Env, user: Address) -> Symbol {
let chosen_key = (user, Symbol::new(&env, "chosen_tier"));
if env.storage().persistent().has(&chosen_key) {
env.storage().persistent().extend_ttl(&chosen_key, LIFETIME_THRESHOLD, BUMP_AMOUNT);
}
env.storage()
.persistent()
.get(&chosen_key)
.unwrap_or(Symbol::new(&env, "TIER_3")) // Default to most conservative
.unwrap_or(Symbol::new(&env, "TIER_3"))
}

/// Get all users in a specific tier
pub fn get_tier_users(env: Env, tier: Symbol) -> Vec<Address> {
let tier_key = (tier, Symbol::new(&env, "users"));
env.storage()
.persistent()
.get(&tier_key)
.unwrap_or(Vec::new(&env))
}



pub fn update_chosen_tier(env: Env, user: Address, new_chosen_tier: Symbol) {

user.require_auth();

/// Update user's chosen tier (risk-based validation)
pub fn update_chosen_tier(env: Env, user: Address, new_chosen_tier: Symbol) {
let tuple_key = (user.clone(), Symbol::new(&env, "risk_tier"));

if let Some(mut risk_data) = env
.storage()
.persistent()
.get::<_, RiskTierData>(&tuple_key)
{
// Risk-based tier access control
// High risk users (>70) can only choose TIER_3 for "opportunity" access

if risk_data.score > 70 {
assert!(
new_chosen_tier == Symbol::new(&env, "TIER_3"),
Expand All @@ -131,17 +149,18 @@ impl RiskTierContract {
}

risk_data.chosen_tier = new_chosen_tier.clone();
risk_data.timestamp = env.ledger().timestamp(); // Update timestamp
risk_data.timestamp = env.ledger().timestamp(); // Atualiza o timestamp

env.storage().persistent().set(&tuple_key, &risk_data);

// Update chosen tier cache

let chosen_key = (user.clone(), Symbol::new(&env, "chosen_tier"));
env.storage()
.persistent()
.set(&chosen_key, &new_chosen_tier);
env.storage().persistent().extend_ttl(&tuple_key, LIFETIME_THRESHOLD, BUMP_AMOUNT);

// Emit Event for Indexers

env.events()
.publish((Symbol::new(&env, "tier_updated"), user), new_chosen_tier);
}
Expand All @@ -158,43 +177,42 @@ impl RiskTierContract {
];

for tier in tiers {
let tier_users = Self::get_tier_users(env.clone(), tier.clone());
stats.set(tier, tier_users.len());
let stats_key = (tier.clone(), Symbol::new(&env, "count"));
let count: u32 = env.storage().persistent().get(&stats_key).unwrap_or(0);
stats.set(tier, count);
}

stats
}
}

/// Check if user can access specific tier based on risk score
/// Following Goldfinch/Maple risk-liquidity mapping methodology
pub fn can_access_tier(env: Env, user: Address, target_tier: Symbol) -> bool {
pub fn can_access_tier(env: Env, user: Address, target_tier: Symbol) -> bool {
let tuple_key = (user, Symbol::new(&env, "risk_tier"));

if let Some(risk_data) = env
.storage()
.persistent()
.get::<_, RiskTierData>(&tuple_key)
{
if let Some(risk_data) = env.storage().persistent().get::<_, RiskTierData>(&tuple_key) {
// Explicação: Renova a vida do score do usuário sempre que o acesso a um tier for validado.
env.storage().persistent().extend_ttl(&tuple_key, LIFETIME_THRESHOLD, BUMP_AMOUNT);
let tier_1 = Symbol::new(&env, "TIER_1");
let tier_2 = Symbol::new(&env, "TIER_2");
let tier_3 = Symbol::new(&env, "TIER_3");

match target_tier {
t if t == tier_1 => risk_data.score <= 30, // Low risk only
t if t == tier_2 => risk_data.score <= 70, // Low to medium risk
t if t == tier_3 => true, // All users (with opportunity badge for high risk)
t if t == tier_1 => risk_data.score <= 30,
t if t == tier_2 => risk_data.score <= 70,
t if t == tier_3 => true,
_ => false,
}
} else {
false // No risk data means no access
false
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use soroban_sdk::{testutils::Address as _, Env};
use soroban_sdk::{testutils::Address as _, Address, Env, Symbol};

#[test]
fn test_set_and_get_risk_tier() {
Expand Down Expand Up @@ -325,26 +343,13 @@ mod tests {
assert!(client.can_access_tier(&user, &tier_3));
}

#[test]
fn test_get_tier_users() {
let env = Env::default();
let contract_id = env.register_contract(None, RiskTierContract);
let client = RiskTierContractClient::new(&env, &contract_id);


let user1 = Address::generate(&env);
let user2 = Address::generate(&env);
let tier_1 = Symbol::new(&env, "TIER_1");

client.set_risk_tier(&user1, &20, &tier_1, &tier_1);
client.set_risk_tier(&user2, &25, &tier_1, &tier_1);

let tier_users = client.get_tier_users(&tier_1);
assert_eq!(tier_users.len(), 2);
}

#[test]
fn test_update_chosen_tier_valid() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register_contract(None, RiskTierContract);
let client = RiskTierContractClient::new(&env, &contract_id);

Expand All @@ -363,6 +368,7 @@ mod tests {
#[should_panic(expected = "High risk users can only access TIER_3")]
fn test_update_chosen_tier_high_risk_restriction() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register_contract(None, RiskTierContract);
let client = RiskTierContractClient::new(&env, &contract_id);

Expand Down
2 changes: 1 addition & 1 deletion risk_score/target/.rustc_info.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"rustc_fingerprint":16478150667837962261,"outputs":{"6432102384495711296":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n/Users/mericcintosun/.rustup/toolchains/stable-aarch64-apple-darwin\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"aarch64\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"unix\"\ntarget_feature=\"aes\"\ntarget_feature=\"crc\"\ntarget_feature=\"dit\"\ntarget_feature=\"dotprod\"\ntarget_feature=\"dpb\"\ntarget_feature=\"dpb2\"\ntarget_feature=\"fcma\"\ntarget_feature=\"fhm\"\ntarget_feature=\"flagm\"\ntarget_feature=\"fp16\"\ntarget_feature=\"frintts\"\ntarget_feature=\"jsconv\"\ntarget_feature=\"lor\"\ntarget_feature=\"lse\"\ntarget_feature=\"neon\"\ntarget_feature=\"paca\"\ntarget_feature=\"pacg\"\ntarget_feature=\"pan\"\ntarget_feature=\"pmuv3\"\ntarget_feature=\"ras\"\ntarget_feature=\"rcpc\"\ntarget_feature=\"rcpc2\"\ntarget_feature=\"rdm\"\ntarget_feature=\"sb\"\ntarget_feature=\"sha2\"\ntarget_feature=\"sha3\"\ntarget_feature=\"ssbs\"\ntarget_feature=\"vh\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"macos\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"apple\"\nunix\n","stderr":""},"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.87.0 (17067e9ac 2025-05-09)\nbinary: rustc\ncommit-hash: 17067e9ac6d7ecb70e50f92c1944e545188d2359\ncommit-date: 2025-05-09\nhost: aarch64-apple-darwin\nrelease: 1.87.0\nLLVM version: 20.1.1\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n/Users/mericcintosun/.rustup/toolchains/stable-aarch64-apple-darwin\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"aarch64\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"unix\"\ntarget_feature=\"aes\"\ntarget_feature=\"crc\"\ntarget_feature=\"dit\"\ntarget_feature=\"dotprod\"\ntarget_feature=\"dpb\"\ntarget_feature=\"dpb2\"\ntarget_feature=\"fcma\"\ntarget_feature=\"fhm\"\ntarget_feature=\"flagm\"\ntarget_feature=\"fp16\"\ntarget_feature=\"frintts\"\ntarget_feature=\"jsconv\"\ntarget_feature=\"lor\"\ntarget_feature=\"lse\"\ntarget_feature=\"neon\"\ntarget_feature=\"paca\"\ntarget_feature=\"pacg\"\ntarget_feature=\"pan\"\ntarget_feature=\"pmuv3\"\ntarget_feature=\"ras\"\ntarget_feature=\"rcpc\"\ntarget_feature=\"rcpc2\"\ntarget_feature=\"rdm\"\ntarget_feature=\"sb\"\ntarget_feature=\"sha2\"\ntarget_feature=\"sha3\"\ntarget_feature=\"ssbs\"\ntarget_feature=\"vh\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"macos\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"apple\"\nunix\n","stderr":""}},"successes":{}}
{"rustc_fingerprint":14548784786673697327,"outputs":{"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.94.1 (e408947bf 2026-03-25)\nbinary: rustc\ncommit-hash: e408947bfd200af42db322daf0fadfe7e26d3bd1\ncommit-date: 2026-03-25\nhost: x86_64-unknown-linux-gnu\nrelease: 1.94.1\nLLVM version: 21.1.8\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/davicf/.rustup/toolchains/stable-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""}},"successes":{}}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This file has an mtime of when this was started.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
7ad1ffdc913b49b0
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"rustc":5391851738765093524,"features":"[]","declared_features":"[\"atomic-polyfill\", \"compile-time-rng\", \"const-random\", \"default\", \"getrandom\", \"nightly-arm-aes\", \"no-rng\", \"runtime-rng\", \"serde\", \"std\"]","target":8470944000320059508,"profile":2241668132362809309,"path":1000150475596497644,"deps":[[966925859616469517,"build_script_build",false,4855633269822001664],[2377604147989930065,"zerocopy",false,1629745417883585268],[3722963349756955755,"once_cell",false,6858396806878814708],[10411997081178400487,"cfg_if",false,11389096365319607192]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/ahash-77223db491905a1a/dep-lib-ahash","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ceabe74dd8f683c4
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"rustc":5391851738765093524,"features":"[]","declared_features":"[\"atomic-polyfill\", \"compile-time-rng\", \"const-random\", \"default\", \"getrandom\", \"nightly-arm-aes\", \"no-rng\", \"runtime-rng\", \"serde\", \"std\"]","target":17883862002600103897,"profile":2225463790103693989,"path":10360560310000106578,"deps":[[5398981501050481332,"version_check",false,18211033694260611887]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/ahash-b7cbf9fb7031edc9/dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This file has an mtime of when this was started.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
00124f8fbbac6243
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"rustc":5391851738765093524,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[966925859616469517,"build_script_build",false,14160433062263368654]],"local":[{"RerunIfChanged":{"output":"debug/build/ahash-d65145b925b1be4a/output","paths":["build.rs"]}}],"rustflags":[],"config":0,"compile_kind":0}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This file has an mtime of when this was started.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ae69acd2c7020d54
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"rustc":5391851738765093524,"features":"[]","declared_features":"[\"atomic-polyfill\", \"compile-time-rng\", \"const-random\", \"default\", \"getrandom\", \"nightly-arm-aes\", \"no-rng\", \"runtime-rng\", \"serde\", \"std\"]","target":8470944000320059508,"profile":15657897354478470176,"path":1000150475596497644,"deps":[[966925859616469517,"build_script_build",false,4855633269822001664],[2377604147989930065,"zerocopy",false,17933514029074762879],[3722963349756955755,"once_cell",false,17388527346529185925],[10411997081178400487,"cfg_if",false,2368829275827904794]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/ahash-eeaa5b62fdd3d021/dep-lib-ahash","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This file has an mtime of when this was started.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
560995d0117a90e9
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"rustc":5391851738765093524,"features":"[\"derive\", \"derive_arbitrary\"]","declared_features":"[\"derive\", \"derive_arbitrary\"]","target":17665432273791891122,"profile":15657897354478470176,"path":12118894668508175445,"deps":[[10187655140533542017,"derive_arbitrary",false,16170204557054330642]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/arbitrary-5b4cc396f7826bae/dep-lib-arbitrary","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This file has an mtime of when this was started.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
63b99aa01c920df3
Loading