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
13 changes: 13 additions & 0 deletions contracts/predictify-hybrid/src/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use crate::fees::{FeeConfig, FeeManager};
use crate::markets::MarketStateManager;
use crate::resolution::MarketResolutionManager;
use alloc::string::ToString;
use crate::audit_trail::{AuditAction, AuditTrailManager};

/// Admin management system for Predictify Hybrid contract
///
Expand Down Expand Up @@ -245,6 +246,8 @@ impl AdminInitializer {
// Log admin action
AdminActionLogger::log_action(env, admin, "initialize", None, Map::new(env), true, None)?;

AuditTrailManager::append_record(env, AuditAction::ContractInitialized, admin.clone(), Map::new(env));

Ok(())
}

Expand Down Expand Up @@ -621,6 +624,7 @@ impl ContractPauseManager {
.persistent()
.set(&Symbol::new(env, CONTRACT_PAUSED_KEY), &true);
EventEmitter::emit_contract_paused(env, admin);
AuditTrailManager::append_record(env, AuditAction::ContractPaused, admin.clone(), Map::new(env));
Ok(())
}

Expand All @@ -639,6 +643,7 @@ impl ContractPauseManager {
.persistent()
.set(&Symbol::new(env, CONTRACT_PAUSED_KEY), &false);
EventEmitter::emit_contract_unpaused(env, admin);
AuditTrailManager::append_record(env, AuditAction::ContractUnpaused, admin.clone(), Map::new(env));
Ok(())
}

Expand Down Expand Up @@ -670,6 +675,7 @@ impl ContractPauseManager {
.persistent()
.set(&Symbol::new(env, "Admin"), new_admin);
EventEmitter::emit_admin_transferred(env, current_admin, new_admin);
AuditTrailManager::append_record(env, AuditAction::AdminTransferred, current_admin.clone(), Map::new(env));
Ok(())
}
}
Expand Down Expand Up @@ -987,6 +993,13 @@ impl AdminRoleManager {
};
EventEmitter::emit_admin_role_assigned(env, admin, &events_role, assigned_by);

let action = if role == AdminRole::SuperAdmin && !env.storage().persistent().has(&key) {
AuditAction::ContractInitialized // Fallback or logic
} else {
AuditAction::AdminAdded
};
AuditTrailManager::append_record(env, AuditAction::AdminRoleUpdated, assigned_by.clone(), Map::new(env));

Ok(())
}

Expand Down
192 changes: 192 additions & 0 deletions contracts/predictify-hybrid/src/audit_trail.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
use soroban_sdk::{contracttype, Address, BytesN, Env, Map, Symbol, String, Vec};

/// Represents the type of sensitive action recorded in the audit trail.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum AuditAction {
// Admin Actions
ContractInitialized,
AdminAdded,
AdminRemoved,
AdminRoleUpdated,
ContractPaused,
ContractUnpaused,
AdminTransferred,

// Market/Event Actions
MarketCreated,
EventCreated,
EventDescriptionUpdated,
EventOutcomesUpdated,
EventCategoryUpdated,
EventTagsUpdated,
EventCancelled,
MarketUpdated,

// Fee Actions
FeesCollected,
FeesWithdrawn,
FeeConfigUpdated,

// Oracle & Config Actions
OracleConfigUpdated,
BetLimitsUpdated,

// Resolution & Disputes
MarketResolved,
DisputeCreated,
DisputeResolved,

// Storage & System
StorageOptimized,
StorageMigrated,
ContractUpgraded,
UpgradeRolledBack,

// Recovery
ErrorRecovered,
PartialRefundExecuted,
}

/// A single record in the immutable, tamper-evident audit trail.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AuditRecord {
pub index: u64,
pub action: AuditAction,
pub actor: Address,
pub timestamp: u64,
pub details: Map<Symbol, String>,
pub prev_record_hash: BytesN<32>,
}

/// Head of the audit trail, tracking the latest state.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AuditTrailHead {
pub latest_index: u64,
pub latest_hash: BytesN<32>,
}

pub struct AuditTrailManager;

impl AuditTrailManager {
/// Storage key for the audit trail head
fn head_key(env: &Env) -> Symbol {
Symbol::new(env, "AUDIT_HEAD")
}

/// Appends a new record to the audit trail.
pub fn append_record(
env: &Env,
action: AuditAction,
actor: Address,
details: Map<Symbol, String>,
) -> u64 {
let mut head: AuditTrailHead = env
.storage()
.persistent()
.get(&Self::head_key(env))
.unwrap_or(AuditTrailHead {
latest_index: 0,
latest_hash: BytesN::from_array(env, &[0u8; 32]),
});

let new_index = head.latest_index + 1;

let record = AuditRecord {
index: new_index,
action,
actor,
timestamp: env.ledger().timestamp(),
details,
prev_record_hash: head.latest_hash.clone(),
};

// Use a tuple key for distinct storage namespace (Symbol, index)
let record_key = (Symbol::new(env, "AUDIT_REC"), new_index);
env.storage().persistent().set(&record_key, &record);

// Instead of xdr, let's just use the Soroban bytes macro or hash a simple representation
// Since we want tamper evidence of the payload, we use ToXdr implemented by the SDK.
use soroban_sdk::xdr::ToXdr;
let record_bytes = record.clone().to_xdr(env);
let new_hash: BytesN<32> = env.crypto().sha256(&record_bytes).into();

head.latest_index = new_index;
head.latest_hash = new_hash;
env.storage().persistent().set(&Self::head_key(env), &head);

new_index
}

/// Retrieves a specific audit record by index.
pub fn get_record(env: &Env, index: u64) -> Option<AuditRecord> {
let record_key = (Symbol::new(env, "AUDIT_REC"), index);
env.storage().persistent().get(&record_key)
}

/// Retrieves the latest records from the audit trail.
pub fn get_latest_records(env: &Env, limit: u64) -> Vec<AuditRecord> {
let head_opt = Self::get_head(env);
if head_opt.is_none() {
return Vec::new(env);
}

let head = head_opt.unwrap();
let mut records = Vec::new(env);
let mut current_index = head.latest_index;
let mut count = 0;

while current_index > 0 && count < limit {
if let Some(record) = Self::get_record(env, current_index) {
records.push_back(record);
}
current_index -= 1;
count += 1;
}

records
}

/// Retrieves the head of the audit trail.
pub fn get_head(env: &Env) -> Option<AuditTrailHead> {
env.storage().persistent().get(&Self::head_key(env))
}

/// Verifies the integrity of the trail from the current head back to a certain depth.
pub fn verify_integrity(env: &Env, depth: u64) -> bool {
let head_opt: Option<AuditTrailHead> = env.storage().persistent().get(&Self::head_key(env));
if head_opt.is_none() {
return true;
}

let head = head_opt.unwrap();
let mut current_index = head.latest_index;
let mut expected_hash = head.latest_hash;
let mut checked = 0;

use soroban_sdk::xdr::ToXdr;

while current_index > 0 && checked < depth {
let record_opt = Self::get_record(env, current_index);
if record_opt.is_none() {
return false;
}

let record = record_opt.unwrap();
let record_bytes = record.clone().to_xdr(env);
let actual_hash: BytesN<32> = env.crypto().sha256(&record_bytes).into();

if actual_hash != expected_hash {
return false;
}

expected_hash = record.prev_record_hash;
current_index -= 1;
checked += 1;
}

true
}
}
14 changes: 14 additions & 0 deletions contracts/predictify-hybrid/src/disputes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,13 @@ impl DisputeManager {
reason_for_event,
);

crate::audit_trail::AuditTrailManager::append_record(
env,
crate::audit_trail::AuditAction::DisputeCreated,
user.clone(),
Map::new(env)
);

Ok(())
}

Expand Down Expand Up @@ -976,6 +983,13 @@ impl DisputeManager {
DisputeUtils::finalize_market_with_resolution(&mut market, final_outcome)?;
MarketStateManager::update_market(env, &market_id, &market);

crate::audit_trail::AuditTrailManager::append_record(
env,
crate::audit_trail::AuditAction::DisputeResolved,
admin.clone(),
Map::new(env)
);

Ok(resolution)
}

Expand Down
28 changes: 28 additions & 0 deletions contracts/predictify-hybrid/src/fees.rs
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,13 @@ impl FeeManager {
&soroban_sdk::String::from_str(env, "platform_fee"),
);

crate::audit_trail::AuditTrailManager::append_record(
env,
crate::audit_trail::AuditAction::FeesCollected,
admin.clone(),
Map::new(env)
);

Ok(fee_amount)
}

Expand Down Expand Up @@ -816,6 +823,13 @@ impl FeeManager {
// Record configuration change
FeeTracker::record_config_change(env, &admin, &new_config)?;

crate::audit_trail::AuditTrailManager::append_record(
env,
crate::audit_trail::AuditAction::FeeConfigUpdated,
admin.clone(),
Map::new(env)
);

Ok(new_config)
}

Expand Down Expand Up @@ -859,6 +873,13 @@ impl FeeManager {
// Record fee structure update
FeeTracker::record_fee_structure_update(env, &admin, &new_fee_tiers)?;

crate::audit_trail::AuditTrailManager::append_record(
env,
crate::audit_trail::AuditAction::FeeConfigUpdated,
admin.clone(),
Map::new(env)
);

Ok(())
}

Expand Down Expand Up @@ -1661,6 +1682,13 @@ impl FeeWithdrawalManager {
now,
);

crate::audit_trail::AuditTrailManager::append_record(
env,
crate::audit_trail::AuditAction::FeesWithdrawn,
admin.clone(),
Map::new(env)
);

Ok(withdrawal_amount)
}
}
Expand Down
Loading
Loading