Skip to content
Merged
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
15 changes: 9 additions & 6 deletions contract/src/leaderboard.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
use soroban_sdk::{Address, Env, Vec};
use crate::storage_types::{DataKey, LeaderboardSnapshot, Season, UserProfile};
use crate::errors::InsightArenaError;
use crate::storage_types::{DataKey, LeaderboardSnapshot, Season, UserProfile};
use soroban_sdk::{Address, Env, Vec};

/// `stake_bonus = floor(stake_xlm / 10)` → `floor(stake_stroops / 10^8 stroops)`.
const STROOPS_PER_STAKE_POINT: i128 = 100_000_000;

// --- Storage & Data Access ---

/// Fetch a leaderboard snapshot for a specific season.
pub fn get_leaderboard(env: &Env, season_id: u32) -> Result<LeaderboardSnapshot, InsightArenaError> {
pub fn get_leaderboard(
env: &Env,
season_id: u32,
) -> Result<LeaderboardSnapshot, InsightArenaError> {
let key = DataKey::Leaderboard(season_id);
env.storage()
.persistent()
Expand Down Expand Up @@ -39,7 +42,7 @@ pub fn calculate_points(stake_amount: i128, correct: u32, total: u32) -> u32 {
let sum = 100_i128.saturating_add(stake_bonus);
let numer = sum.saturating_mul(correct).saturating_mul(2_i128);
let res = numer / total;

if res < 0 {
return 0;
}
Expand Down Expand Up @@ -147,9 +150,9 @@ mod leaderboard_tests {

#[test]
fn get_user_season_points_unknown_season_and_user_returns_zero() {
use crate::InsightArenaContract;
use soroban_sdk::testutils::Address as _;
use soroban_sdk::{Address, Env};
use crate::InsightArenaContract;

let env = Env::default();
let contract_id = env.register(InsightArenaContract, ());
Expand All @@ -159,4 +162,4 @@ mod leaderboard_tests {
});
assert_eq!(points, 0);
}
}
}
49 changes: 49 additions & 0 deletions contract/src/liquidity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,31 @@ pub fn calculate_swap_output(

// ── Liquidity Management ──────────────────────────────────────────────────────

/// Calculate LP tokens to mint for a deposit
pub fn calculate_lp_tokens(
deposit_amount: i128,
total_liquidity: i128,
total_lp_supply: i128,
) -> Result<i128, InsightArenaError> {
if deposit_amount <= 0 {
return Err(InsightArenaError::InvalidInput);
}

// First deposit: mint tokens equal to deposit
if total_lp_supply == 0 || total_liquidity == 0 {
return Ok(deposit_amount);
}

// Subsequent deposits: mint proportionally
let lp_tokens = deposit_amount
.checked_mul(total_lp_supply)
.ok_or(InsightArenaError::Overflow)?
.checked_div(total_liquidity)
.ok_or(InsightArenaError::Overflow)?;

Ok(lp_tokens)
}

// TODO: add_liquidity
// TODO: remove_liquidity

Expand Down Expand Up @@ -98,4 +123,28 @@ mod tests {
let result = calculate_swap_output(i128::MAX, 1000, 1000, 30);
assert_eq!(result, Err(InsightArenaError::Overflow));
}

#[test]
fn test_calculate_lp_tokens_first_deposit() {
// Deposit: 1000, Liquidity: 0, Supply: 0 → Expected: 1000
assert_eq!(calculate_lp_tokens(1000, 0, 0), Ok(1000));
}

#[test]
fn test_calculate_lp_tokens_second_deposit_equal() {
// Deposit: 1000, Liquidity: 1000, Supply: 1000 → Expected: 1000
assert_eq!(calculate_lp_tokens(1000, 1000, 1000), Ok(1000));
}

#[test]
fn test_calculate_lp_tokens_second_deposit_half() {
// Deposit: 500, Liquidity: 1000, Supply: 1000 → Expected: 500
assert_eq!(calculate_lp_tokens(500, 1000, 1000), Ok(500));
}

#[test]
fn test_calculate_lp_tokens_second_deposit_double() {
// Deposit: 2000, Liquidity: 1000, Supply: 1000 → Expected: 2000
assert_eq!(calculate_lp_tokens(2000, 1000, 1000), Ok(2000));
}
}
236 changes: 1 addition & 235 deletions contract/src/reputation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,238 +87,4 @@ pub fn get_creator_stats(env: Env, creator: Address) -> Result<CreatorStats, Ins

// ── Tests ─────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod reputation_tests {
use soroban_sdk::testutils::{Address as _, Ledger as _};
use soroban_sdk::{symbol_short, vec, Address, Env, String, Symbol};

use crate::market::CreateMarketParams;
use crate::storage_types::CreatorStats;
use crate::{InsightArenaContract, InsightArenaContractClient};

use super::calculate_creator_reputation;

fn register_token(env: &Env) -> Address {
let token_admin = Address::generate(env);
env.register_stellar_asset_contract_v2(token_admin)
.address()
}

fn deploy(env: &Env) -> (InsightArenaContractClient<'_>, Address, Address) {
let id = env.register(InsightArenaContract, ());
let client = InsightArenaContractClient::new(env, &id);
let admin = Address::generate(env);
let oracle = Address::generate(env);
let xlm_token = register_token(env);
env.mock_all_auths();
client.initialize(&admin, &oracle, &200_u32, &xlm_token);
(client, admin, oracle)
}

fn default_params(env: &Env) -> CreateMarketParams {
let now = env.ledger().timestamp();
CreateMarketParams {
title: String::from_str(env, "Test market"),
description: String::from_str(env, "desc"),
category: Symbol::new(env, "Sports"),
outcomes: vec![env, symbol_short!("yes"), symbol_short!("no")],
end_time: now + 1000,
resolution_time: now + 2000,
dispute_window: 86_400,
creator_fee_bps: 100,
min_stake: 10_000_000,
max_stake: 100_000_000,
is_public: true,
}
}

// ── Pure formula tests ────────────────────────────────────────────────────

#[test]
fn reputation_zero_for_new_creator() {
let stats = CreatorStats {
markets_created: 0,
markets_resolved: 0,
average_participant_count: 0,
dispute_count: 0,
reputation_score: 0,
};
assert_eq!(calculate_creator_reputation(&stats), 0);
}

#[test]
fn reputation_perfect_score_no_disputes() {
// 10/10 resolved, 100 avg participants → 600 + 200 - 0 = 800
let stats = CreatorStats {
markets_created: 10,
markets_resolved: 10,
average_participant_count: 100,
dispute_count: 0,
reputation_score: 0,
};
assert_eq!(calculate_creator_reputation(&stats), 800);
}

#[test]
fn reputation_clamped_to_1000() {
let stats = CreatorStats {
markets_created: 1,
markets_resolved: 1,
average_participant_count: 300, // bonus capped at 200
dispute_count: 0,
reputation_score: 0,
};
// 600 + 200 = 800
assert_eq!(calculate_creator_reputation(&stats), 800);
}

#[test]
fn reputation_dispute_penalty_capped_at_200() {
// 10 * 50 = 500, capped at 200 → 600 + 0 - 200 = 400
let stats = CreatorStats {
markets_created: 10,
markets_resolved: 10,
average_participant_count: 0,
dispute_count: 10,
reputation_score: 0,
};
assert_eq!(calculate_creator_reputation(&stats), 400);
}

#[test]
fn reputation_never_underflows() {
// 0 resolved, max disputes → saturating_sub → 0
let stats = CreatorStats {
markets_created: 10,
markets_resolved: 0,
average_participant_count: 0,
dispute_count: 100,
reputation_score: 0,
};
assert_eq!(calculate_creator_reputation(&stats), 0);
}

#[test]
fn reputation_partial_resolution() {
// 5/10 * 600 = 300, 10 * 2 = 20, 1 * 50 = 50 → 270
let stats = CreatorStats {
markets_created: 10,
markets_resolved: 5,
average_participant_count: 10,
dispute_count: 1,
reputation_score: 0,
};
assert_eq!(calculate_creator_reputation(&stats), 270);
}

#[test]
fn reputation_participation_bonus_capped_at_200() {
let stats = CreatorStats {
markets_created: 1,
markets_resolved: 1,
average_participant_count: 200, // 200 * 2 = 400, capped at 200
dispute_count: 0,
reputation_score: 0,
};
assert_eq!(calculate_creator_reputation(&stats), 800);
}

// ── Integration tests ─────────────────────────────────────────────────────

#[test]
fn get_creator_stats_returns_default_for_unknown_creator() {
let env = Env::default();
env.mock_all_auths();
let (client, _, _) = deploy(&env);
let unknown = Address::generate(&env);

let stats = client.get_creator_stats(&unknown);
assert_eq!(stats.markets_created, 0);
assert_eq!(stats.markets_resolved, 0);
assert_eq!(stats.reputation_score, 0);
}

#[test]
fn stats_updated_on_market_creation() {
let env = Env::default();
env.mock_all_auths();
let (client, _, _) = deploy(&env);
let creator = Address::generate(&env);

client.create_market(&creator, &default_params(&env));

let stats = client.get_creator_stats(&creator);
assert_eq!(stats.markets_created, 1);
assert_eq!(stats.markets_resolved, 0);
}

#[test]
fn stats_updated_on_market_resolution() {
let env = Env::default();
env.mock_all_auths();
let (client, _, oracle) = deploy(&env);
let creator = Address::generate(&env);

let id = client.create_market(&creator, &default_params(&env));
env.ledger().set_timestamp(env.ledger().timestamp() + 2000);
client.resolve_market(&oracle, &id, &symbol_short!("yes"));

let stats = client.get_creator_stats(&creator);
assert_eq!(stats.markets_created, 1);
assert_eq!(stats.markets_resolved, 1);
}

#[test]
fn stats_accumulate_across_multiple_markets() {
let env = Env::default();
env.mock_all_auths();
let (client, _, oracle) = deploy(&env);
let creator = Address::generate(&env);

let id1 = client.create_market(&creator, &default_params(&env));
let id2 = client.create_market(&creator, &default_params(&env));

let stats = client.get_creator_stats(&creator);
assert_eq!(stats.markets_created, 2);

env.ledger().set_timestamp(env.ledger().timestamp() + 2000);
client.resolve_market(&oracle, &id1, &symbol_short!("yes"));
client.resolve_market(&oracle, &id2, &symbol_short!("no"));

let stats = client.get_creator_stats(&creator);
assert_eq!(stats.markets_resolved, 2);
}

#[test]
fn reputation_score_stored_in_stats() {
let env = Env::default();
env.mock_all_auths();
let (client, _, oracle) = deploy(&env);
let creator = Address::generate(&env);

let id = client.create_market(&creator, &default_params(&env));
env.ledger().set_timestamp(env.ledger().timestamp() + 2000);
client.resolve_market(&oracle, &id, &symbol_short!("yes"));

let stats = client.get_creator_stats(&creator);
// 1/1 resolved = 600, 0 participants, 0 disputes → 600
assert_eq!(stats.reputation_score, 600);
}

#[test]
fn reputation_score_always_in_range() {
let env = Env::default();
env.mock_all_auths();
let (client, _, oracle) = deploy(&env);
let creator = Address::generate(&env);

for _ in 0..3 {
let id = client.create_market(&creator, &default_params(&env));
env.ledger().set_timestamp(env.ledger().timestamp() + 2000);
client.resolve_market(&oracle, &id, &symbol_short!("yes"));
}

let stats = client.get_creator_stats(&creator);
assert!(stats.reputation_score <= 1000);
}
}
// Tests have been moved to tests/reputation_tests.rs
6 changes: 3 additions & 3 deletions contract/src/season.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use soroban_sdk::{symbol_short, Address, Env, Vec};
use crate::leaderboard;
use crate::config::{self, PERSISTENT_BUMP, PERSISTENT_THRESHOLD};
use crate::errors::InsightArenaError;
use crate::escrow;
use crate::leaderboard;
use crate::storage_types::{
DataKey, LeaderboardEntry, LeaderboardSnapshot, RewardPayout, Season, UserProfile,
};
use crate::ttl;
use soroban_sdk::{symbol_short, Address, Env, Vec};

fn bump_season(env: &Env, season_id: u32) {
ttl::extend_season_ttl(env, season_id);
Expand Down Expand Up @@ -399,7 +399,7 @@ pub fn update_leaderboard(
}

let updated_at = env.ledger().timestamp();

// Call the persistence layer in leaderboard.rs
leaderboard::store_snapshot(
env,
Expand Down
5 changes: 3 additions & 2 deletions contract/src/season_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,8 @@ fn test_points_accumulate_across_markets() {
);

let profile = client.get_user_stats(&winner);
let expected_points = calculate_expected_points(50_000_000, 1, 1) + calculate_expected_points(30_000_000, 2, 2);
let expected_points =
calculate_expected_points(50_000_000, 1, 1) + calculate_expected_points(30_000_000, 2, 2);

assert_eq!(profile.season_points, expected_points);
assert_eq!(profile.total_winnings, first_payout + second_payout);
Expand All @@ -392,4 +393,4 @@ fn calculate_expected_points(stake_amount: i128, correct: u32, total: u32) -> u3
} else {
res as u32
}
}
}
1 change: 0 additions & 1 deletion contract/src/ttl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,3 @@ pub fn extend_season_ttl(env: &Env, season_id: u32) {
);
}
}

Loading
Loading