diff --git a/contracts/src/common.rs b/contracts/src/common.rs
index af8b9ac..ea76974 100644
--- a/contracts/src/common.rs
+++ b/contracts/src/common.rs
@@ -3,15 +3,14 @@ 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, Round, RoundPhase,
+ PendingWinningsUpdatedAtKey,
};
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.
@@ -140,7 +139,6 @@ pub fn sort_addresses(addresses: Vec
) -> Vec {
/// 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 existing: i128 = env.storage().persistent().get(&key).unwrap_or(0);
let new_pending = payout_add(existing, amount)?;
diff --git a/contracts/src/config.rs b/contracts/src/config.rs
index 09dc0e8..83069e2 100644
--- a/contracts/src/config.rs
+++ b/contracts/src/config.rs
@@ -1,15 +1,6 @@
// 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,
@@ -23,7 +14,7 @@ use crate::common::{
};
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};
diff --git a/contracts/src/contract.rs b/contracts/src/contract.rs
index 84e00e9..56c8e8d 100644
--- a/contracts/src/contract.rs
+++ b/contracts/src/contract.rs
@@ -1068,7 +1068,7 @@ impl VirtualTokenContract {
/// Anyone may call `finalize_round` after the dispute window expires to
/// distribute winnings to winners (normal settlement outcome).
pub fn finalize_round(env: Env, round_id: u64) -> Result<(), ContractError> {
- settlement::finalize_round(env, round_id)
+ settlement::_finalize_round(env, round_id)
}
pub fn get_active_round(env: Env) -> Option {
@@ -1076,7 +1076,7 @@ impl VirtualTokenContract {
}
pub fn get_one_sided_policy(env: Env) -> OneSidedPolicy {
- let active_round: Option = env.storage().persistent().get(&DataKey::ActiveRound);
+ let active_round: Option = env.storage().persistent().get(&DataKeyCore::ActiveRound);
if let Some(round) = active_round {
settlement::_select_one_sided_policy(&round)
} else {
diff --git a/contracts/src/settlement.rs b/contracts/src/settlement.rs
index dcc8911..3892ac1 100644
--- a/contracts/src/settlement.rs
+++ b/contracts/src/settlement.rs
@@ -1,12 +1,13 @@
// SPDX-License-Identifier: MIT
+extern crate alloc;
+use alloc::vec::Vec as StdVec;
use crate::admin::{
_ensure_not_paused, _load_attestation_config, _load_deviation_config, _require_supported_schema,
};
use crate::common::{
_accumulate_pending, _emit_action_rejected, _extend_persistent_ttl, _set_balance, balance,
payout_add, payout_mul, sort_addresses, DEFAULT_ARCHIVE_RETENTION,
- DEFAULT_ORACLE_TIMESTAMP_SKEW, SECONDS_PER_LEDGER,
- payout_add, payout_mul, sort_addresses, DEFAULT_ARCHIVE_RETENTION, MAX_ORACLE_OBSERVATIONS,
+ DEFAULT_ORACLE_TIMESTAMP_SKEW, SECONDS_PER_LEDGER, MAX_ORACLE_OBSERVATIONS,
TTL_BUMP_AMOUNT, TTL_BUMP_THRESHOLD,
};
use crate::config::{
@@ -24,9 +25,10 @@ use crate::types::{
OraclePayload, OracleQuorumConfig, PrecisionCommitment, PrecisionPayoutPolicy,
PrecisionPrediction, PriceSample, PendingWinningsUpdatedAtKey, Round, RoundArchiveStatus,
RoundMode, TwapSamplesKey, UserOutcomeType, UserPosition, UserRoundOutcome, UserStats,
+ OneSidedPolicy, Policy,
};
use soroban_sdk::xdr::ToXdr;
-use soroban_sdk::{symbol_short, Address, Bytes, Env, Map, Vec};
+use soroban_sdk::{symbol_short, Address, Bytes, Env, Map, Vec, IntoVal, Val};
/// Cancels the active round and deterministically refunds all participant stakes.
pub fn cancel_round(env: Env, _reason: u32) -> Result<(), ContractError> {
@@ -994,8 +996,10 @@ fn _settle_round_with_price(
env,
round,
RoundArchiveStatus::FallbackRefund,
- payload.price,
+ final_price,
&threshold_participants,
+ 0,
+ None,
);
_refund_under_threshold(env, round, &threshold_participants)?;
#[allow(deprecated)]
@@ -1034,19 +1038,17 @@ fn _settle_round_with_price(
env,
round,
RoundArchiveStatus::Resolved,
- payload.price,
- &participants,
final_price,
- participant_count,
+ &participants,
fee_amount,
- payload.confidence,
+ confidence,
);
// Mode-scoped position cleanup (eliminates redundant storage delete lookups)
match round.mode {
RoundMode::UpDown => {
- for i in 0..raw_participants.len() {
- if let Some(user) = raw_participants.get(i) {
+ for i in 0..participants.len() {
+ if let Some(user) = participants.get(i) {
env.storage()
.persistent()
.remove(&DataKeyScoped::Position(round_id, user));
@@ -1054,8 +1056,8 @@ fn _settle_round_with_price(
}
}
RoundMode::Precision => {
- for i in 0..raw_participants.len() {
- if let Some(user) = raw_participants.get(i) {
+ for i in 0..participants.len() {
+ if let Some(user) = participants.get(i) {
env.storage()
.persistent()
.remove(&DataKeyScoped::PrecisionPosition(round_id, user.clone()));
@@ -1104,7 +1106,7 @@ pub fn _resolve_updown_mode(
final_price: u128,
skip_payout: bool,
) -> Result<(bool, i128), ContractError> {
- let participants = sort_addresses(raw_participants.clone());
+ let participants = sort_addresses(participants.clone());
// Pure price-direction classification and one-sided check delegated to
// settlement_math for auditability and golden-vector coverage.
@@ -1126,7 +1128,7 @@ pub fn _resolve_updown_mode(
let positions: Map = if participants.is_empty() {
env.storage()
.persistent()
- .get(&DataKey::UpDownPositions)
+ .get(&DataKeyCore::UpDownPositions)
.unwrap_or(Map::new(env))
} else {
Map::new(env)
@@ -1377,7 +1379,7 @@ pub fn _resolve_precision_mode(
round_id: u64,
final_price: u128,
skip_payout: bool,
-) -> Result {
+) -> Result<(i128, i128), ContractError> {
let mut participants: Vec = env
.storage()
.persistent()
@@ -1821,7 +1823,7 @@ pub fn _archive_round(
round: &Round,
status: RoundArchiveStatus,
final_price: u128,
- participants: &[Address],
+ participants: &Vec,
fee_amount: i128,
confidence: Option,
) {
@@ -1843,7 +1845,7 @@ pub fn _archive_round(
// Record per-user participation index for paginated history queries.
for i in 0..participants.len() {
if let Some(user) = participants.get(i) {
- let index_key = DataKey::UserArchivedRoundIds(user.clone());
+ let index_key = DataKeyScoped::UserArchivedRoundIds(user.clone());
let mut user_rounds: Vec = env
.storage()
.persistent()
@@ -1911,25 +1913,27 @@ pub fn _archive_round(
let fee_model_value: u32 = _read_fee_model(env) as u32;
#[allow(deprecated)]
+ let event_data: soroban_sdk::Vec = soroban_sdk::vec![
+ env,
+ 0u32.into_val(env),
+ round.round_id.into_val(env),
+ status_val.into_val(env),
+ (round.mode.clone() as u32).into_val(env),
+ round.price_start.into_val(env),
+ final_price.into_val(env),
+ round.pool_up.into_val(env),
+ round.pool_down.into_val(env),
+ (participant_count as u32).into_val(env),
+ total_pot.into_val(env),
+ fee_amount.into_val(env),
+ settled_at_ledger.into_val(env),
+ confidence.into_val(env),
+ status_val.into_val(env),
+ fee_model_value.into_val(env),
+ ];
env.events().publish(
(symbol_short!("round"), symbol_short!("summary")),
- (
- 0u32,
- round.round_id,
- status_val,
- round.mode.clone() as u32,
- round.price_start,
- final_price,
- round.pool_up,
- round.pool_down,
- participant_count,
- total_pot,
- fee_amount,
- settled_at_ledger,
- confidence,
- status_val,
- fee_model_value,
- ),
+ event_data,
);
let mut recent: Vec = env
@@ -2295,4 +2299,82 @@ pub fn _update_stats_loss(env: &Env, user: Address) -> Result<(), ContractError>
crate::leaderboard::_update_leaderboards(env, user.clone());
crate::leaderboard::_update_season_stats_loss(env, user)?;
Ok(())
-}
\ No newline at end of file
+}
+
+fn _enforce_heartbeat_health(env: &Env, oracle: &Address) -> Result<(), ContractError> {
+ let hb_config = crate::admin::_load_hb_config(env);
+ if hb_config.strict_mode {
+ let hb_blocked = _check_heartbeat_health_blocked(env, &hb_config);
+ if hb_blocked {
+ if hb_config.override_armed {
+ // Let it pass (override is armed).
+ } else {
+ _emit_action_rejected(
+ env,
+ oracle,
+ symbol_short!("resolve"),
+ ContractError::OracleNotLive,
+ );
+ return Err(ContractError::OracleNotLive);
+ }
+ }
+ }
+ Ok(())
+}
+
+/// Deterministically selects the active one-sided settlement policy for a round.
+pub fn _select_one_sided_policy(_round: &Round) -> OneSidedPolicy {
+ OneSidedPolicy::Refund
+}
+
+/// Applies deterministic one-sided settlement policy for degenerate markets.
+pub fn _apply_one_sided_policy(
+ env: &Env,
+ round: &Round,
+ policy: OneSidedPolicy,
+ participants: &Vec,
+ positions: &Option