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
10 changes: 10 additions & 0 deletions contracts/src/access_control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,13 +189,23 @@ pub fn is_allowlisted(env: Env, user: Address) -> bool {
.has(&DataKeyScoped::Allowlisted(user))
}

/// Alias for `is_allowlisted`.
pub fn is_user_allowlisted(env: Env, user: Address) -> bool {
is_allowlisted(env, user)
}

/// Returns whether `user` is denylisted (read-only).
pub fn is_denylisted(env: Env, user: Address) -> bool {
env.storage()
.persistent()
.has(&DataKeyScoped::Denylisted(user))
}

/// Alias for `is_denylisted`.
pub fn is_user_denylisted(env: Env, user: Address) -> bool {
is_denylisted(env, user)
}

/// Returns the resolved access state for `user` (read-only).
///
/// Denylist takes precedence over allowlist. An address that is neither marked
Expand Down
45 changes: 27 additions & 18 deletions contracts/src/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::common::{
};
use crate::errors::ContractError;
use crate::types::{
AttestationConfig, AttestationConfigKey, DataKey, DataKeyCore, DataKeyExt,
AttestationConfig, AttestationConfigKey, DataKeyCore, DataKeyScoped,
DeviationConfig, DeviationConfigKey, DeviationReferenceMode, HbGateConfig, HbGateKey,
OracleHeartbeatRecord, OracleQuorumConfig, PolicyAction, ProtocolHealthStatus, Round,
RuntimeMode, PENDING_WINNINGS_EXPIRY_KEY, PendingWinningsUpdatedAtKey,
Expand Down Expand Up @@ -827,6 +827,12 @@ pub fn get_protocol_health(env: Env) -> ProtocolHealthStatus {
issues += 1;
}

let access_restricted: bool = env
.storage()
.persistent()
.get(&DataKeyCore::AccessControlEnabled)
.unwrap_or(false);

let status_code = if paused {
1u32 // PAUSED
} else if issues > 1 {
Expand All @@ -837,6 +843,8 @@ pub fn get_protocol_health(env: Env) -> ProtocolHealthStatus {
3u32 // ROUND_STALE
} else if !has_active_round {
4u32 // NO_ACTIVE_ROUND
} else if access_restricted {
6u32 // ACCESS_RESTRICTED
} else {
0u32 // HEALTHY
};
Expand Down Expand Up @@ -932,6 +940,7 @@ pub fn _policy_gate(env: &Env, action: PolicyAction) -> Result<(), ContractError
PolicyAction::Claim | PolicyAction::AdminConfig | PolicyAction::Settlement => {
mode == RuntimeMode::FullyPaused
}
_ => mode == RuntimeMode::FullyPaused,
};
if blocked {
return Err(ContractError::ContractPaused);
Expand Down Expand Up @@ -1000,11 +1009,11 @@ pub fn _is_ttl_touch_allowed(key: &DataKeyCore) -> bool {
| DataKeyCore::MigratedToV3
| DataKeyCore::ArchiveRetention
| DataKeyCore::RoundTemplate
| DataKeyCore::Ext(DataKeyExt::LeaderboardWins)
| DataKeyCore::Ext(DataKeyExt::LeaderboardStreak)
| DataKeyCore::Ext(DataKeyExt::SeasonId)
| DataKeyCore::Ext(DataKeyExt::SeasonLeaderboardWins)
| DataKeyCore::Ext(DataKeyExt::SeasonLeaderboardStreak)
| DataKeyCore::LeaderboardWins
| DataKeyCore::LeaderboardStreak
| DataKeyCore::SeasonId
| DataKeyCore::SeasonLeaderboardWins
| DataKeyCore::SeasonLeaderboardStreak
| DataKeyCore::LastRoundId
| DataKeyCore::OracleRotationProposal
| DataKeyCore::MintLimitConfig
Expand Down Expand Up @@ -1044,9 +1053,9 @@ pub fn batch_touch_ttl(env: Env, keys: Vec<DataKeyCore>) -> Result<u32, Contract
&env,
&admin,
symbol_short!("batch_t"),
ContractError::UnsupportedDataKeyForTtlTouch,
ContractError::UnsupportedSchemaVersion,
);
return Err(ContractError::UnsupportedDataKeyForTtlTouch);
return Err(ContractError::UnsupportedSchemaVersion);
}
if env.storage().persistent().has(&key) {
env.storage()
Expand Down Expand Up @@ -1136,12 +1145,12 @@ pub(crate) fn _validate_quorum_config(cfg: &OracleQuorumConfig) -> Result<(), Co
if cfg.min_observations < DEFAULT_ORACLE_QUORUM_MIN_OBSERVATIONS
|| cfg.min_observations > MAX_ORACLE_OBSERVATIONS
{
return Err(ContractError::TooFewObservations);
return Err(ContractError::InvalidMinParticipants);
}
if cfg.quorum_threshold < DEFAULT_ORACLE_QUORUM_THRESHOLD
|| cfg.quorum_threshold > cfg.min_observations
{
return Err(ContractError::InsufficientOracleQuorum);
return Err(ContractError::InvalidMinParticipants);
}
if cfg.outlier_threshold_bps == 0 || cfg.outlier_threshold_bps > 10_000 {
return Err(ContractError::WindowOutOfRange);
Expand Down Expand Up @@ -1201,22 +1210,22 @@ pub fn reclaim_expired_pending_winnings(env: Env, user: Address) -> Result<i128,
&env,
&admin,
symbol_short!("reclaim"),
ContractError::ExpiryNotConfigured,
ContractError::WindowOutOfRange,
);
return Err(ContractError::ExpiryNotConfigured);
return Err(ContractError::WindowOutOfRange);
}

// Read pending winnings.
let pending_key = DataKey::PendingWinnings(user.clone());
let pending_key = DataKeyScoped::PendingWinnings(user.clone());
let pending: i128 = env.storage().persistent().get(&pending_key).unwrap_or(0);
if pending == 0 {
_emit_action_rejected(
&env,
&admin,
symbol_short!("reclaim"),
ContractError::PendingWinningsNotFound,
ContractError::InsufficientBalance,
);
return Err(ContractError::PendingWinningsNotFound);
return Err(ContractError::InsufficientBalance);
}

// Read the ledger when this entry was last updated.
Expand All @@ -1225,7 +1234,7 @@ pub fn reclaim_expired_pending_winnings(env: Env, user: Address) -> Result<i128,
.storage()
.persistent()
.get(&updated_key)
.ok_or(ContractError::PendingWinningsNotFound)?;
.ok_or(ContractError::InsufficientBalance)?;

let current_ledger = env.ledger().sequence();
let age = current_ledger.saturating_sub(updated_at);
Expand All @@ -1235,9 +1244,9 @@ pub fn reclaim_expired_pending_winnings(env: Env, user: Address) -> Result<i128,
&env,
&admin,
symbol_short!("reclaim"),
ContractError::PendingWinningsNotExpired,
ContractError::PendingWinningsCapExceeded,
);
return Err(ContractError::PendingWinningsNotExpired);
return Err(ContractError::PendingWinningsCapExceeded);
}

// CEI: remove storage keys before transferring.
Expand Down
2 changes: 1 addition & 1 deletion contracts/src/betting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -912,7 +912,7 @@ pub fn mint_initial(env: Env, user: Address) -> i128 {
}
}
_ => {
soroban_sdk::panic_with_error!(&env, ContractError::EpochBudgetExceeded);
soroban_sdk::panic_with_error!(&env, ContractError::MintLimitExceeded);
}
}
}
Expand Down
31 changes: 16 additions & 15 deletions contracts/src/common.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
// SPDX-License-Identifier: MIT
extern crate alloc;
use alloc::vec::Vec as StdVec;
use crate::errors::ContractError;
use crate::types::{
ConfigChangeKind, ConfigChangePayload, DataKey, PendingWinningsUpdatedAtKey, Round, RoundPhase,
ConfigChangeKind, ConfigChangePayload, DataKeyCore, DataKeyScoped, PendingWinningsUpdatedAtKey, Round, RoundPhase,
};
use soroban_sdk::{symbol_short, Address, Env, IntoVal, Symbol, Val, Vec};

pub const DEFAULT_PENDING_WINNINGS_EXPIRY: u32 = 0; // 0 = disabled
pub const MIN_PENDING_WINNINGS_EXPIRY: u32 = 128; // ~10 min at 5s ledgers
pub const MAX_PENDING_WINNINGS_EXPIRY: u32 = 1_000_000; // ~58 days
use crate::types::{ConfigChangeKind, ConfigChangePayload, DataKeyCore, DataKeyScoped, Round, RoundPhase};
use soroban_sdk::{symbol_short, Address, Env, IntoVal, Symbol, Val, Vec};

// ─── DataKey overflow workaround (DataKey has 51 variants, XDR limit is 50) ──
// Moved out of DataKey to get under the limit.
Expand Down Expand Up @@ -58,6 +54,7 @@ pub const DEFAULT_CLOSE_BUFFER_LEDGERS: u32 = 0;
pub const MAX_BET_WINDOW_LEDGERS: u32 = 1_440;
pub const MAX_RUN_WINDOW_LEDGERS: u32 = 2_880;
pub const MAX_CLOSE_BUFFER_LEDGERS: u32 = 1_440;
pub const DEFAULT_GOV_PROPOSAL_TTL_LEDGERS: u32 = 100;

// ─── Oracle deviation guardrails ─────────────────────────────────────────────
pub const MAX_ORACLE_DEVIATION_BPS: u32 = 100_000;
Expand Down Expand Up @@ -126,22 +123,26 @@ pub fn sort_addresses(addresses: Vec<Address>) -> Vec<Address> {
if addresses.len() <= 1 {
return addresses;
}
let mut native_vec: StdVec<Address> = StdVec::with_capacity(addresses.len() as usize);
for addr in addresses.iter() {
native_vec.push(addr);
}
native_vec.sort_unstable();
let mut sorted = Vec::new(addresses.env());
for addr in native_vec {
sorted.push_back(addr);
for addr in addresses.iter() {
let mut inserted = false;
for i in 0..sorted.len() {
if addr < sorted.get(i).unwrap() {
sorted.insert(i, addr.clone());
inserted = true;
break;
}
}
if !inserted {
sorted.push_back(addr);
}
}
sorted
}

/// Accumulates `amount` into a user's pending winnings, enforcing the cap if set (Issue #120).
pub fn _accumulate_pending(env: &Env, user: Address, amount: i128) -> Result<(), ContractError> {
let key = DataKey::PendingWinnings(user.clone());
let key = DataKeyScoped::PendingWinnings(user);
let key = DataKeyScoped::PendingWinnings(user.clone());
let existing: i128 = env.storage().persistent().get(&key).unwrap_or(0);
let new_pending = payout_add(existing, amount)?;

Expand Down Expand Up @@ -204,7 +205,7 @@ pub fn _derive_round_phase(ledger_sequence: u32, round: &Round) -> RoundPhase {
pub fn _enforce_min_bet(env: &Env, amount: i128) -> Result<(), ContractError> {
if let Some(min_bet) = env.storage().persistent().get::<_, i128>(&DataKeyCore::MinBet) {
if amount < min_bet {
return Err(ContractError::BelowMinBet);
return Err(ContractError::InvalidBetAmount);
}
}
Ok(())
Expand Down
36 changes: 21 additions & 15 deletions contracts/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,21 @@
// SPDX-License-Identifier: MIT
use crate::admin::{_ensure_normal_mode, _ensure_not_paused, _require_supported_schema};
use crate::common::{
_emit_action_rejected, _emit_config_updated, _extend_persistent_ttl, _set_balance, balance,
payout_add, BPS_DENOMINATOR, CONFIG_TIMELOCK_LEDGERS, DEFAULT_ARCHIVE_RETENTION,
DEFAULT_BET_WINDOW_LEDGERS, DEFAULT_CLOSE_BUFFER_LEDGERS, DEFAULT_MAX_PRECISION_PARTICIPANTS,
DEFAULT_ORACLE_STALE_THRESHOLD, DEFAULT_ORACLE_TIMESTAMP_SKEW, DEFAULT_RUN_WINDOW_LEDGERS,
MAX_ARCHIVE_RETENTION, MAX_BET_WINDOW_LEDGERS, MAX_CLOSE_BUFFER_LEDGERS, MAX_MIN_PARTICIPANTS,
MAX_ORACLE_DEVIATION_BPS, MAX_ORACLE_STALE_THRESHOLD, MAX_ORACLE_TIMESTAMP_SKEW,
MAX_PRECISION_PARTICIPANTS_LIMIT, MAX_PROTOCOL_FEE_BPS, MAX_RUN_WINDOW_LEDGERS,
MAX_START_PRICE, MIN_ARCHIVE_RETENTION, MIN_CAP_VALUE, MIN_ORACLE_STALE_THRESHOLD,
MIN_ORACLE_TIMESTAMP_SKEW, MIN_START_PRICE,
_emit_action_rejected, _emit_config_updated, _extend_persistent_ttl, _extend_ttl_symbol,
_set_balance, balance, payout_add, BPS_DENOMINATOR, CONFIG_TIMELOCK_LEDGERS,
DEFAULT_ARCHIVE_RETENTION, DEFAULT_BET_WINDOW_LEDGERS, DEFAULT_CLOSE_BUFFER_LEDGERS,
DEFAULT_DISPUTE_LEDGERS, DEFAULT_MAX_PRECISION_PARTICIPANTS, DEFAULT_ORACLE_STALE_THRESHOLD,
DEFAULT_PENDING_WINNINGS_EXPIRY, DEFAULT_RUN_WINDOW_LEDGERS, MAX_ARCHIVE_RETENTION,
MAX_BET_WINDOW_LEDGERS, MAX_CLOSE_BUFFER_LEDGERS, MAX_DISPUTE_LEDGERS, MAX_MIN_PARTICIPANTS,
MAX_ORACLE_DEVIATION_BPS, MAX_ORACLE_STALE_THRESHOLD, MAX_PENDING_WINNINGS_EXPIRY,
MAX_PRECISION_PARTICIPANTS_LIMIT, MAX_PROTOCOL_FEE_BPS, MAX_RUN_WINDOW_LEDGERS,
MAX_START_PRICE, MIN_ARCHIVE_RETENTION, MIN_CAP_VALUE, MIN_ORACLE_STALE_THRESHOLD,
DEFAULT_ORACLE_TIMESTAMP_SKEW, DEFAULT_PENDING_WINNINGS_EXPIRY, DEFAULT_RUN_WINDOW_LEDGERS,
MAX_ARCHIVE_RETENTION, MAX_BET_WINDOW_LEDGERS, MAX_CLOSE_BUFFER_LEDGERS, MAX_DISPUTE_LEDGERS,
MAX_MIN_PARTICIPANTS, MAX_ORACLE_DEVIATION_BPS, MAX_ORACLE_STALE_THRESHOLD,
MAX_ORACLE_TIMESTAMP_SKEW, MAX_PENDING_WINNINGS_EXPIRY, MAX_PRECISION_PARTICIPANTS_LIMIT,
MAX_PROTOCOL_FEE_BPS, MAX_RUN_WINDOW_LEDGERS, MAX_START_PRICE, MIN_ARCHIVE_RETENTION,
MIN_CAP_VALUE, MIN_ORACLE_STALE_THRESHOLD, MIN_ORACLE_TIMESTAMP_SKEW,
MIN_PENDING_WINNINGS_EXPIRY, MIN_START_PRICE,
};
use crate::errors::ContractError;
use crate::types::{
ConfigChangeKind, ConfigChangePayload, DataKey, DataKeyCore, DataKeyScoped, FeeModel,
ConfigChangeKind, ConfigChangePayload, DataKeyCore, DataKeyScoped, FeeModel,
PendingConfigChange, PrecisionPayoutPolicy, RoundTemplate, PENDING_WINNINGS_EXPIRY_KEY,
};
use soroban_sdk::{symbol_short, Address, Env, Symbol};
Expand Down Expand Up @@ -847,6 +839,7 @@ pub fn set_early_cashout_bps(env: Env, bps: Option<u32>) -> Result<(), ContractE
}

let key = DataKeyCore::EarlyCashoutBps;
let old_bps: Option<u32> = env.storage().persistent().get(&key);
if let Some(v) = bps {
env.storage().persistent().set(&key, &v);
_extend_persistent_ttl(&env, &key);
Expand Down Expand Up @@ -1272,6 +1265,9 @@ pub fn _current_config_payload(env: &Env, kind: &ConfigChangeKind) -> ConfigChan
.unwrap_or(DEFAULT_DISPUTE_LEDGERS),
),
ConfigChangeKind::FeeModel => ConfigChangePayload::FeeModel(_read_fee_model(env)),
ConfigChangeKind::EarlyCashoutBps => ConfigChangePayload::EarlyCashoutBps(
env.storage().persistent().get(&DataKeyCore::EarlyCashoutBps),
),
}
}

Expand Down Expand Up @@ -1519,6 +1515,16 @@ pub fn _apply_config_payload(
env.storage().persistent().set(&key, max);
_extend_persistent_ttl(env, &key);
}
(ConfigChangeKind::EarlyCashoutBps, ConfigChangePayload::EarlyCashoutBps(bps)) => {
let key = DataKeyCore::EarlyCashoutBps;
if let Some(v) = bps {
env.storage().persistent().set(&key, v);
_extend_persistent_ttl(env, &key);
} else {
env.storage().persistent().remove(&key);
}
}
_ => {}
}
_emit_config_updated(env, kind.clone(), old_value, payload.clone());
Ok(())
Expand Down
Loading