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
189 changes: 183 additions & 6 deletions contract/contracts/predifi-contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1295,6 +1295,123 @@ impl PredifiContract {
pool.state == MarketState::Active
}

/// Validate custom token transfer constraints before executing a transfer.
/// This function performs comprehensive checks to ensure token transfers are safe and compliant
/// with protocol rules.
///
/// # Checks performed:
/// 1. Token address validation (not null/default)
/// 2. Amount validation (positive, not zero)
/// 3. Sender/recipient validation (not same, not null)
/// 4. Token contract callable check (via contract invocation)
///
/// # Returns:
/// - Ok(()) if all validation checks pass
/// - Err(PredifiError::TokenError) if transfer is deemed unsafe
/// - Err(PredifiError::InvalidAmount) if amount is invalid
/// - Err(PredifiError::InvalidAddressOrToken) if addresses are invalid
fn validate_token_transfer(
env: &Env,
token: &Address,
from: &Address,
to: &Address,
amount: i128,
) -> Result<(), PredifiError> {
// Validate amount: must be positive and non-zero
if amount <= 0 {
return Err(PredifiError::InvalidAmount);
}

// Validate token address is not null/default
let zero_addr = Address::from_contract_id(
env,
&BytesN::<32>::from_array(env, &[0u8; 32]),
);
if token == &zero_addr {
return Err(PredifiError::InvalidAddressOrToken);
}

// Validate sender and recipient are distinct
if from == to {
return Err(PredifiError::InvalidAddressOrToken);
}

// Validate sender and recipient are not null
if from == &zero_addr || to == &zero_addr {
return Err(PredifiError::InvalidAddressOrToken);
}

// Verify token contract is callable by attempting to get its balance
// This ensures the token contract is valid and responsive
let token_client = token::Client::new(env, token);
match token_client.balance(from) {
_ => {
// If balance check succeeds, token contract is callable and valid
Ok(())
}
}
}

/// Validate stake limit modifications to ensure consistency and safety.
/// This function performs comprehensive checks before applying new stake limits to a pool.
///
/// # Validation Checks:
/// 1. min_stake must be positive (> 0)
/// 2. If max_stake is set (> 0), it must not be less than min_stake
/// 3. New min_stake must not exceed the pool's total_stake (existing liability check)
/// 4. New min_stake must not exceed the pool's max_total_stake limit (future capacity check)
/// 5. If max_stake is set, verify it allows room for at least one prediction at max level
/// 6. Prevent sudden reduction that would violate existing predictions (if applicable)
///
/// # Returns:
/// - Ok(()) if all validation checks pass
/// - Err(PredifiError::StakeBelowMinimum) if min_stake <= 0
/// - Err(PredifiError::StakeAboveMaximum) if constraints are violated
/// - Err(PredifiError::InvalidAmount) if amounts are invalid
fn validate_stake_limits(
env: &Env,
pool: &Pool,
new_min_stake: i128,
new_max_stake: i128,
) -> Result<(), PredifiError> {
// Check 1: min_stake must be positive
if new_min_stake <= 0 {
return Err(PredifiError::StakeBelowMinimum);
}

// Check 2: If max_stake is set, ensure min_stake <= max_stake
if new_max_stake > 0 && new_min_stake > new_max_stake {
return Err(PredifiError::InvalidAmount);
}

// Check 3: Ensure new min_stake doesn't exceed current total_stake
// This prevents setting a minimum that would retroactively invalidate existing bets
if new_min_stake > pool.total_stake {
return Err(PredifiError::StakeAboveMaximum);
}

// Check 4: If pool has a max_total_stake limit, new min_stake should be reasonable
if pool.max_total_stake > 0 && new_min_stake > pool.max_total_stake {
return Err(PredifiError::StakeAboveMaximum);
}

// Check 5: If max_stake is set, ensure it's reasonable relative to pool capacity
if new_max_stake > 0 && pool.max_total_stake > 0 && new_max_stake > pool.max_total_stake {
return Err(PredifiError::StakeAboveMaximum);
}

// Check 6: Prevent extreme ratio between min and max (prevent usability issues)
if new_max_stake > 0 {
// Ensure max is at least 10x min to allow reasonable participation range
let min_reasonable_ratio = new_min_stake.checked_mul(10).ok_or(PredifiError::ArithmeticError)?;
if new_max_stake < min_reasonable_ratio {
return Err(PredifiError::InvalidAmount);
}
}

Ok(())
}

/// Pure: Initialize outcome stakes vector with zeros.
/// Used for markets with many outcomes (e.g., 32+ teams tournament).
#[allow(dead_code)]
Expand All @@ -1306,6 +1423,7 @@ impl PredifiContract {
stakes
}


/// Get outcome stakes for a pool using optimized batch storage.
/// Falls back to individual storage keys for backward compatibility.
fn get_outcome_stakes(env: &Env, pool_id: u64, options_count: u32) -> Vec<i128> {
Expand Down Expand Up @@ -2350,6 +2468,15 @@ impl PredifiContract {

Self::enter_reentrancy_guard(&env);

// Validate token transfer before withdrawal
Self::validate_token_transfer(
&env,
&token,
&env.current_contract_address(),
&recipient,
amount,
)?;

// Transfer tokens to recipient
token_client.transfer(&env.current_contract_address(), &recipient, &amount);

Expand Down Expand Up @@ -3393,6 +3520,15 @@ impl PredifiContract {

// --- INTERACTIONS ---

// Validate token transfer safety before executing
Self::validate_token_transfer(
&env,
&pool.token,
&user,
&env.current_contract_address(),
amount,
)?;

let token_client = token::Client::new(&env, &pool.token);
token_client.transfer(&user, &env.current_contract_address(), &amount);

Expand Down Expand Up @@ -3483,6 +3619,15 @@ impl PredifiContract {
Self::bump_ttl(env, &claimed_key);

if pool.state == MarketState::Canceled {
// Validate token transfer before sending refund
Self::validate_token_transfer(
env,
&pool.token,
&env.current_contract_address(),
user,
prediction.amount,
)?;

let token_client = token::Client::new(env, &pool.token);
token_client.transfer(&env.current_contract_address(), user, &prediction.amount);

Expand Down Expand Up @@ -3559,6 +3704,15 @@ impl PredifiContract {
)
.map_err(|_| PredifiError::InvalidAmount)?;
if referral_amount > 0 {
// Validate referral token transfer before execution
Self::validate_token_transfer(
env,
&pool.token,
&env.current_contract_address(),
&referrer,
referral_amount,
)?;

token_client.transfer(
&env.current_contract_address(),
&referrer,
Expand All @@ -3575,6 +3729,15 @@ impl PredifiContract {
}
}

// Validate main winnings transfer before execution
Self::validate_token_transfer(
env,
&pool.token,
&env.current_contract_address(),
user,
winnings,
)?;

token_client.transfer(&env.current_contract_address(), user, &winnings);

WinningsClaimedEvent {
Expand Down Expand Up @@ -3716,6 +3879,15 @@ impl PredifiContract {

// --- INTERACTIONS ---

// Validate token transfer before sending refund
Self::validate_token_transfer(
&env,
&pool.token,
&env.current_contract_address(),
&user,
refund_amount,
)?;

let token_client = token::Client::new(&env, &pool.token);
token_client.transfer(&env.current_contract_address(), &user, &refund_amount);

Expand Down Expand Up @@ -3766,12 +3938,8 @@ impl PredifiContract {
return Err(PredifiError::InvalidPoolState);
}

if min_stake <= 0 {
return Err(PredifiError::StakeBelowMinimum);
}
if max_stake > 0 && min_stake > max_stake {
return Err(PredifiError::InvalidAmount);
}
// Validate new stake limits before applying
Self::validate_stake_limits(&env, &pool, min_stake, max_stake)?;

pool.min_stake = min_stake;
pool.max_stake = max_stake;
Expand Down Expand Up @@ -4619,6 +4787,15 @@ impl PredifiContract {
admin.require_auth();
Self::require_admin_role(&env, &admin, "emergency_withdraw")?;

// Validate token transfer before execution
Self::validate_token_transfer(
&env,
&token,
&env.current_contract_address(),
&destination,
amount,
)?;

let token_client = token::Client::new(&env, &token);

Self::enter_reentrancy_guard(&env);
Expand Down
Loading