Skip to content

Commit fb741ed

Browse files
Merge pull request #473 from Amas-01/feature/audit-trail
feat(contract): audit trail completeness
2 parents 0ff2b71 + d45af58 commit fb741ed

File tree

6 files changed

+530
-8
lines changed

6 files changed

+530
-8
lines changed

contracts/predictify-hybrid/src/admin.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use crate::fees::{FeeConfig, FeeManager};
1111
use crate::markets::MarketStateManager;
1212
use crate::resolution::MarketResolutionManager;
1313
use alloc::string::ToString;
14+
use crate::audit_trail::{AuditAction, AuditTrailManager};
1415

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

249+
AuditTrailManager::append_record(env, AuditAction::ContractInitialized, admin.clone(), Map::new(env));
250+
248251
Ok(())
249252
}
250253

@@ -621,6 +624,7 @@ impl ContractPauseManager {
621624
.persistent()
622625
.set(&Symbol::new(env, CONTRACT_PAUSED_KEY), &true);
623626
EventEmitter::emit_contract_paused(env, admin);
627+
AuditTrailManager::append_record(env, AuditAction::ContractPaused, admin.clone(), Map::new(env));
624628
Ok(())
625629
}
626630

@@ -639,6 +643,7 @@ impl ContractPauseManager {
639643
.persistent()
640644
.set(&Symbol::new(env, CONTRACT_PAUSED_KEY), &false);
641645
EventEmitter::emit_contract_unpaused(env, admin);
646+
AuditTrailManager::append_record(env, AuditAction::ContractUnpaused, admin.clone(), Map::new(env));
642647
Ok(())
643648
}
644649

@@ -670,6 +675,7 @@ impl ContractPauseManager {
670675
.persistent()
671676
.set(&Symbol::new(env, "Admin"), new_admin);
672677
EventEmitter::emit_admin_transferred(env, current_admin, new_admin);
678+
AuditTrailManager::append_record(env, AuditAction::AdminTransferred, current_admin.clone(), Map::new(env));
673679
Ok(())
674680
}
675681
}
@@ -987,6 +993,13 @@ impl AdminRoleManager {
987993
};
988994
EventEmitter::emit_admin_role_assigned(env, admin, &events_role, assigned_by);
989995

996+
let action = if role == AdminRole::SuperAdmin && !env.storage().persistent().has(&key) {
997+
AuditAction::ContractInitialized // Fallback or logic
998+
} else {
999+
AuditAction::AdminAdded
1000+
};
1001+
AuditTrailManager::append_record(env, AuditAction::AdminRoleUpdated, assigned_by.clone(), Map::new(env));
1002+
9901003
Ok(())
9911004
}
9921005

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
use soroban_sdk::{contracttype, Address, BytesN, Env, Map, Symbol, String, Vec};
2+
3+
/// Represents the type of sensitive action recorded in the audit trail.
4+
#[contracttype]
5+
#[derive(Clone, Debug, Eq, PartialEq)]
6+
pub enum AuditAction {
7+
// Admin Actions
8+
ContractInitialized,
9+
AdminAdded,
10+
AdminRemoved,
11+
AdminRoleUpdated,
12+
ContractPaused,
13+
ContractUnpaused,
14+
AdminTransferred,
15+
16+
// Market/Event Actions
17+
MarketCreated,
18+
EventCreated,
19+
EventDescriptionUpdated,
20+
EventOutcomesUpdated,
21+
EventCategoryUpdated,
22+
EventTagsUpdated,
23+
EventCancelled,
24+
MarketUpdated,
25+
26+
// Fee Actions
27+
FeesCollected,
28+
FeesWithdrawn,
29+
FeeConfigUpdated,
30+
31+
// Oracle & Config Actions
32+
OracleConfigUpdated,
33+
BetLimitsUpdated,
34+
35+
// Resolution & Disputes
36+
MarketResolved,
37+
DisputeCreated,
38+
DisputeResolved,
39+
40+
// Storage & System
41+
StorageOptimized,
42+
StorageMigrated,
43+
ContractUpgraded,
44+
UpgradeRolledBack,
45+
46+
// Recovery
47+
ErrorRecovered,
48+
PartialRefundExecuted,
49+
}
50+
51+
/// A single record in the immutable, tamper-evident audit trail.
52+
#[contracttype]
53+
#[derive(Clone, Debug, Eq, PartialEq)]
54+
pub struct AuditRecord {
55+
pub index: u64,
56+
pub action: AuditAction,
57+
pub actor: Address,
58+
pub timestamp: u64,
59+
pub details: Map<Symbol, String>,
60+
pub prev_record_hash: BytesN<32>,
61+
}
62+
63+
/// Head of the audit trail, tracking the latest state.
64+
#[contracttype]
65+
#[derive(Clone, Debug, Eq, PartialEq)]
66+
pub struct AuditTrailHead {
67+
pub latest_index: u64,
68+
pub latest_hash: BytesN<32>,
69+
}
70+
71+
pub struct AuditTrailManager;
72+
73+
impl AuditTrailManager {
74+
/// Storage key for the audit trail head
75+
fn head_key(env: &Env) -> Symbol {
76+
Symbol::new(env, "AUDIT_HEAD")
77+
}
78+
79+
/// Appends a new record to the audit trail.
80+
pub fn append_record(
81+
env: &Env,
82+
action: AuditAction,
83+
actor: Address,
84+
details: Map<Symbol, String>,
85+
) -> u64 {
86+
let mut head: AuditTrailHead = env
87+
.storage()
88+
.persistent()
89+
.get(&Self::head_key(env))
90+
.unwrap_or(AuditTrailHead {
91+
latest_index: 0,
92+
latest_hash: BytesN::from_array(env, &[0u8; 32]),
93+
});
94+
95+
let new_index = head.latest_index + 1;
96+
97+
let record = AuditRecord {
98+
index: new_index,
99+
action,
100+
actor,
101+
timestamp: env.ledger().timestamp(),
102+
details,
103+
prev_record_hash: head.latest_hash.clone(),
104+
};
105+
106+
// Use a tuple key for distinct storage namespace (Symbol, index)
107+
let record_key = (Symbol::new(env, "AUDIT_REC"), new_index);
108+
env.storage().persistent().set(&record_key, &record);
109+
110+
// Instead of xdr, let's just use the Soroban bytes macro or hash a simple representation
111+
// Since we want tamper evidence of the payload, we use ToXdr implemented by the SDK.
112+
use soroban_sdk::xdr::ToXdr;
113+
let record_bytes = record.clone().to_xdr(env);
114+
let new_hash: BytesN<32> = env.crypto().sha256(&record_bytes).into();
115+
116+
head.latest_index = new_index;
117+
head.latest_hash = new_hash;
118+
env.storage().persistent().set(&Self::head_key(env), &head);
119+
120+
new_index
121+
}
122+
123+
/// Retrieves a specific audit record by index.
124+
pub fn get_record(env: &Env, index: u64) -> Option<AuditRecord> {
125+
let record_key = (Symbol::new(env, "AUDIT_REC"), index);
126+
env.storage().persistent().get(&record_key)
127+
}
128+
129+
/// Retrieves the latest records from the audit trail.
130+
pub fn get_latest_records(env: &Env, limit: u64) -> Vec<AuditRecord> {
131+
let head_opt = Self::get_head(env);
132+
if head_opt.is_none() {
133+
return Vec::new(env);
134+
}
135+
136+
let head = head_opt.unwrap();
137+
let mut records = Vec::new(env);
138+
let mut current_index = head.latest_index;
139+
let mut count = 0;
140+
141+
while current_index > 0 && count < limit {
142+
if let Some(record) = Self::get_record(env, current_index) {
143+
records.push_back(record);
144+
}
145+
current_index -= 1;
146+
count += 1;
147+
}
148+
149+
records
150+
}
151+
152+
/// Retrieves the head of the audit trail.
153+
pub fn get_head(env: &Env) -> Option<AuditTrailHead> {
154+
env.storage().persistent().get(&Self::head_key(env))
155+
}
156+
157+
/// Verifies the integrity of the trail from the current head back to a certain depth.
158+
pub fn verify_integrity(env: &Env, depth: u64) -> bool {
159+
let head_opt: Option<AuditTrailHead> = env.storage().persistent().get(&Self::head_key(env));
160+
if head_opt.is_none() {
161+
return true;
162+
}
163+
164+
let head = head_opt.unwrap();
165+
let mut current_index = head.latest_index;
166+
let mut expected_hash = head.latest_hash;
167+
let mut checked = 0;
168+
169+
use soroban_sdk::xdr::ToXdr;
170+
171+
while current_index > 0 && checked < depth {
172+
let record_opt = Self::get_record(env, current_index);
173+
if record_opt.is_none() {
174+
return false;
175+
}
176+
177+
let record = record_opt.unwrap();
178+
let record_bytes = record.clone().to_xdr(env);
179+
let actual_hash: BytesN<32> = env.crypto().sha256(&record_bytes).into();
180+
181+
if actual_hash != expected_hash {
182+
return false;
183+
}
184+
185+
expected_hash = record.prev_record_hash;
186+
current_index -= 1;
187+
checked += 1;
188+
}
189+
190+
true
191+
}
192+
}

contracts/predictify-hybrid/src/disputes.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -855,6 +855,13 @@ impl DisputeManager {
855855
reason_for_event,
856856
);
857857

858+
crate::audit_trail::AuditTrailManager::append_record(
859+
env,
860+
crate::audit_trail::AuditAction::DisputeCreated,
861+
user.clone(),
862+
Map::new(env)
863+
);
864+
858865
Ok(())
859866
}
860867

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

986+
crate::audit_trail::AuditTrailManager::append_record(
987+
env,
988+
crate::audit_trail::AuditAction::DisputeResolved,
989+
admin.clone(),
990+
Map::new(env)
991+
);
992+
979993
Ok(resolution)
980994
}
981995

contracts/predictify-hybrid/src/fees.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,13 @@ impl FeeManager {
757757
&soroban_sdk::String::from_str(env, "platform_fee"),
758758
);
759759

760+
crate::audit_trail::AuditTrailManager::append_record(
761+
env,
762+
crate::audit_trail::AuditAction::FeesCollected,
763+
admin.clone(),
764+
Map::new(env)
765+
);
766+
760767
Ok(fee_amount)
761768
}
762769

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

826+
crate::audit_trail::AuditTrailManager::append_record(
827+
env,
828+
crate::audit_trail::AuditAction::FeeConfigUpdated,
829+
admin.clone(),
830+
Map::new(env)
831+
);
832+
819833
Ok(new_config)
820834
}
821835

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

876+
crate::audit_trail::AuditTrailManager::append_record(
877+
env,
878+
crate::audit_trail::AuditAction::FeeConfigUpdated,
879+
admin.clone(),
880+
Map::new(env)
881+
);
882+
862883
Ok(())
863884
}
864885

@@ -1661,6 +1682,13 @@ impl FeeWithdrawalManager {
16611682
now,
16621683
);
16631684

1685+
crate::audit_trail::AuditTrailManager::append_record(
1686+
env,
1687+
crate::audit_trail::AuditAction::FeesWithdrawn,
1688+
admin.clone(),
1689+
Map::new(env)
1690+
);
1691+
16641692
Ok(withdrawal_amount)
16651693
}
16661694
}

0 commit comments

Comments
 (0)