diff --git a/.gitignore b/.gitignore
index d58ebed..7625e09 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,5 +8,8 @@ engine-bridge/dist/
# Secrets
.env
+# Database
+*.db
+
# OS
.DS_Store
diff --git a/engine-core/src/core/access.rs b/engine-core/src/core/access.rs
new file mode 100644
index 0000000..c6661c7
--- /dev/null
+++ b/engine-core/src/core/access.rs
@@ -0,0 +1,201 @@
+use soroban_sdk::{contracterror, panic_with_error, symbol_short, vec, Address, BytesN, Env, Symbol, Vec};
+use crate::event_struct::{MOD_CORE, ACT_INIT};
+use crate::event_utils::publish_event;
+
+const KEY_INIT: Symbol = symbol_short!("C_INIT");
+const KEY_ADMIN: Symbol = symbol_short!("C_ADMIN");
+const KEY_OPERATRS: Symbol = symbol_short!("C_OPERS");
+const KEY_AUDITORS: Symbol = symbol_short!("C_AUDIT");
+
+#[contracterror]
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+pub enum AccessError {
+ Unauthorized = 1,
+ AlreadyInit = 2,
+ NotInitialized = 3,
+ InvalidAdmin = 4,
+ InvalidOperator = 5,
+ InvalidAuditor = 6,
+}
+
+pub const ROLE_ADMIN: u32 = 0;
+pub const ROLE_OPERATOR: u32 = 1;
+pub const ROLE_AUDITOR: u32 = 2;
+
+pub fn initialize(env: &Env, admin: &Address, operators: Vec
, auditors: Vec) {
+ if is_initialized(env) {
+ panic_with_error!(env, AccessError::AlreadyInit);
+ }
+
+ admin.require_auth();
+
+ env.storage().instance().set(&KEY_INIT, &true);
+ env.storage().instance().set(&KEY_ADMIN, admin);
+
+ let mut op_vec: Vec = vec![env];
+ for o in operators.iter() {
+ op_vec.push_back(o);
+ }
+ env.storage().instance().set(&KEY_OPERATRS, &op_vec);
+
+ let mut au_vec: Vec = vec![env];
+ for a in auditors.iter() {
+ au_vec.push_back(a);
+ }
+ env.storage().instance().set(&KEY_AUDITORS, &au_vec);
+
+ publish_event(
+ env,
+ MOD_CORE | ACT_INIT,
+ 0,
+ BytesN::from_array(env, &[0u8; 32]),
+ );
+}
+
+pub fn is_initialized(env: &Env) -> bool {
+ env.storage().instance().has(&KEY_INIT)
+}
+
+pub fn require_initialized(env: &Env) {
+ if !is_initialized(env) {
+ panic_with_error!(env, AccessError::NotInitialized);
+ }
+}
+
+pub fn require_role(env: &Env, caller: &Address, role: u32) {
+ caller.require_auth();
+ require_initialized(env);
+
+ let ok = match role {
+ ROLE_ADMIN => {
+ let admin: Address = env.storage().instance().get(&KEY_ADMIN)
+ .unwrap_or_else(|| panic_with_error!(env, AccessError::NotInitialized));
+ admin == caller.clone()
+ }
+ ROLE_OPERATOR => {
+ let operators: Vec = env.storage().instance().get(&KEY_OPERATRS)
+ .unwrap_or_else(|| vec![env]);
+ operators.contains(caller)
+ }
+ ROLE_AUDITOR => {
+ let auditors: Vec = env.storage().instance().get(&KEY_AUDITORS)
+ .unwrap_or_else(|| vec![env]);
+ auditors.contains(caller)
+ }
+ _ => false,
+ };
+
+ if !ok {
+ panic_with_error!(env, AccessError::Unauthorized);
+ }
+}
+
+pub fn get_admin(env: &Env) -> Option {
+ env.storage().instance().get(&KEY_ADMIN)
+}
+
+pub fn get_operators(env: &Env) -> Vec {
+ env.storage().instance().get(&KEY_OPERATRS).unwrap_or(vec![env])
+}
+
+pub fn get_auditors(env: &Env) -> Vec {
+ env.storage().instance().get(&KEY_AUDITORS).unwrap_or(vec![env])
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use soroban_sdk::{testutils::Address as _, vec, Env};
+
+ #[soroban_sdk::contract]
+ pub struct TestContract;
+
+ #[soroban_sdk::contractimpl]
+ impl TestContract {}
+
+ fn setup(env: &Env) -> (Address, Address, Vec, Vec) {
+ let admin = Address::generate(env);
+ let op1 = Address::generate(env);
+ let au1 = Address::generate(env);
+ let operators = vec![env, op1.clone()];
+ let auditors = vec![env, au1.clone()];
+ (admin, op1, operators, auditors)
+ }
+
+ #[test]
+ fn initialize_sets_admin() {
+ let env = Env::default();
+ env.mock_all_auths();
+ let contract_id = env.register_contract(None, TestContract);
+ let (admin, _, operators, auditors) = setup(&env);
+ env.as_contract(&contract_id, || {
+ initialize(&env, &admin, operators, auditors);
+ assert!(is_initialized(&env));
+ assert_eq!(get_admin(&env).unwrap(), admin);
+ });
+ }
+
+ #[test]
+ #[should_panic]
+ fn double_init_rejected() {
+ let env = Env::default();
+ env.mock_all_auths();
+ let contract_id = env.register_contract(None, TestContract);
+ let (admin, _, operators, auditors) = setup(&env);
+ env.as_contract(&contract_id, || {
+ initialize(&env, &admin, operators.clone(), auditors.clone());
+ initialize(&env, &admin, operators, auditors);
+ });
+ }
+
+ #[test]
+ fn admin_role_authorized() {
+ let env = Env::default();
+ env.mock_all_auths();
+ let contract_id = env.register_contract(None, TestContract);
+ let (admin, _, operators, auditors) = setup(&env);
+ env.as_contract(&contract_id, || {
+ initialize(&env, &admin, operators, auditors);
+ require_role(&env, &admin, ROLE_ADMIN);
+ });
+ }
+
+ #[test]
+ fn operator_role_authorized() {
+ let env = Env::default();
+ env.mock_all_auths();
+ let contract_id = env.register_contract(None, TestContract);
+ let (admin, op1, operators, auditors) = setup(&env);
+ env.as_contract(&contract_id, || {
+ initialize(&env, &admin, operators, auditors);
+ require_role(&env, &op1, ROLE_OPERATOR);
+ });
+ }
+
+ #[test]
+ #[should_panic]
+ fn operator_not_admin() {
+ let env = Env::default();
+ env.mock_all_auths();
+ let contract_id = env.register_contract(None, TestContract);
+ let (admin, op1, operators, auditors) = setup(&env);
+ let rogue = Address::generate(&env);
+ env.as_contract(&contract_id, || {
+ initialize(&env, &admin, operators, auditors);
+ require_role(&env, &rogue, ROLE_ADMIN);
+ });
+ }
+
+ #[test]
+ fn auditor_role_authorized() {
+ let env = Env::default();
+ env.mock_all_auths();
+ let contract_id = env.register_contract(None, TestContract);
+ let (admin, _, operators, auditors) = setup(&env);
+ let au1 = auditors.get(0).unwrap();
+ env.as_contract(&contract_id, || {
+ initialize(&env, &admin, operators, auditors);
+ require_role(&env, &au1, ROLE_AUDITOR);
+ });
+ }
+}
diff --git a/engine-core/src/core/guards.rs b/engine-core/src/core/guards.rs
new file mode 100644
index 0000000..fb1fea6
--- /dev/null
+++ b/engine-core/src/core/guards.rs
@@ -0,0 +1,103 @@
+use soroban_sdk::{contracterror, panic_with_error, symbol_short, Env, Symbol};
+
+const KEY_REENTRY: Symbol = symbol_short!("C_REENTR");
+
+#[contracterror]
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+pub enum GuardError {
+ ReentrancyDetected = 1,
+}
+
+pub struct ReentrancyGuard;
+
+impl ReentrancyGuard {
+ pub fn new(env: &Env) -> Self {
+ if env.storage().temporary().has(&KEY_REENTRY) {
+ panic_with_error!(env, GuardError::ReentrancyDetected);
+ }
+ env.storage().temporary().set(&KEY_REENTRY, &true);
+ Self
+ }
+}
+
+impl Drop for ReentrancyGuard {
+ fn drop(&mut self) {
+ }
+}
+
+pub fn with_guard(env: &Env, f: impl FnOnce() -> T) -> T {
+ let _guard = ReentrancyGuard::new(env);
+ let result = f();
+ env.storage().temporary().remove(&KEY_REENTRY);
+ result
+}
+
+pub fn enter_guard(env: &Env) {
+ if env.storage().temporary().has(&KEY_REENTRY) {
+ panic_with_error!(env, GuardError::ReentrancyDetected);
+ }
+ env.storage().temporary().set(&KEY_REENTRY, &true);
+}
+
+pub fn exit_guard(env: &Env) {
+ env.storage().temporary().remove(&KEY_REENTRY);
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use soroban_sdk::Env;
+
+ #[soroban_sdk::contract]
+ pub struct TestContract;
+
+ #[soroban_sdk::contractimpl]
+ impl TestContract {}
+
+ #[test]
+ fn guard_enter_and_exit() {
+ let env = Env::default();
+ let contract_id = env.register_contract(None, TestContract);
+ env.as_contract(&contract_id, || {
+ enter_guard(&env);
+ assert!(env.storage().temporary().has(&KEY_REENTRY));
+ exit_guard(&env);
+ assert!(!env.storage().temporary().has(&KEY_REENTRY));
+ });
+ }
+
+ #[test]
+ #[should_panic]
+ fn double_enter_detected() {
+ let env = Env::default();
+ let contract_id = env.register_contract(None, TestContract);
+ env.as_contract(&contract_id, || {
+ enter_guard(&env);
+ enter_guard(&env);
+ });
+ }
+
+ #[test]
+ fn with_guard_wraps_call() {
+ let env = Env::default();
+ let contract_id = env.register_contract(None, TestContract);
+ env.as_contract(&contract_id, || {
+ let result = with_guard(&env, || 42);
+ assert_eq!(result, 42);
+ assert!(!env.storage().temporary().has(&KEY_REENTRY));
+ });
+ }
+
+ #[test]
+ fn reentrancy_guard_drop_releases() {
+ let env = Env::default();
+ let contract_id = env.register_contract(None, TestContract);
+ env.as_contract(&contract_id, || {
+ {
+ let _guard = ReentrancyGuard::new(&env);
+ assert!(env.storage().temporary().has(&KEY_REENTRY));
+ }
+ assert!(!env.storage().temporary().has(&KEY_REENTRY));
+ });
+ }
+}
diff --git a/engine-core/src/core/state.rs b/engine-core/src/core/state.rs
new file mode 100644
index 0000000..a113fb4
--- /dev/null
+++ b/engine-core/src/core/state.rs
@@ -0,0 +1,144 @@
+use soroban_sdk::{contracterror, panic_with_error, symbol_short, BytesN, Env, Symbol};
+use crate::event_struct::{MOD_CORE, ACT_TRANSITION};
+use crate::event_utils::publish_event;
+
+const KEY_STATE: Symbol = symbol_short!("PROTO_ST");
+
+#[contracterror]
+#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
+pub enum StateError {
+ InvalidTransition = 1,
+ AlreadyInState = 2,
+}
+
+#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
+pub enum ProtocolState {
+ Pending = 0,
+ Active = 1,
+ Settled = 2,
+ Closed = 3,
+}
+
+impl ProtocolState {
+ fn from_u32(v: u32) -> Self {
+ match v {
+ 0 => ProtocolState::Pending,
+ 1 => ProtocolState::Active,
+ 2 => ProtocolState::Settled,
+ 3 => ProtocolState::Closed,
+ _ => ProtocolState::Pending,
+ }
+ }
+}
+
+fn allowed_transition(from: ProtocolState, to: ProtocolState) -> bool {
+ match (from, to) {
+ (ProtocolState::Pending, ProtocolState::Active) => true,
+ (ProtocolState::Pending, ProtocolState::Closed) => true,
+ (ProtocolState::Active, ProtocolState::Settled) => true,
+ (ProtocolState::Active, ProtocolState::Closed) => true,
+ (ProtocolState::Settled, ProtocolState::Closed) => true,
+ _ => false,
+ }
+}
+
+pub fn init(env: &Env) {
+ env.storage().instance().set(&KEY_STATE, &ProtocolState::Pending);
+}
+
+pub fn get_state(env: &Env) -> ProtocolState {
+ let raw: u32 = env.storage().instance().get(&KEY_STATE).unwrap_or(0);
+ ProtocolState::from_u32(raw)
+}
+
+pub fn transition_to(env: &Env, next: ProtocolState) {
+ let current = get_state(env);
+ if current == next {
+ panic_with_error!(env, StateError::AlreadyInState);
+ }
+ if !allowed_transition(current, next) {
+ panic_with_error!(env, StateError::InvalidTransition);
+ }
+ env.storage().instance().set(&KEY_STATE, &next);
+ publish_event(
+ env,
+ MOD_CORE | ACT_TRANSITION,
+ current as u64,
+ BytesN::from_array(env, &[0u8; 32]),
+ );
+}
+
+pub fn require_state(env: &Env, expected: ProtocolState) {
+ let current = get_state(env);
+ if current != expected {
+ panic_with_error!(env, StateError::InvalidTransition);
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use soroban_sdk::Env;
+
+ #[soroban_sdk::contract]
+ pub struct TestContract;
+
+ #[soroban_sdk::contractimpl]
+ impl TestContract {}
+
+ #[test]
+ fn initial_state_is_pending() {
+ let env = Env::default();
+ let contract_id = env.register_contract(None, TestContract);
+ env.as_contract(&contract_id, || {
+ init(&env);
+ assert_eq!(get_state(&env), ProtocolState::Pending);
+ });
+ }
+
+ #[test]
+ fn transition_pending_to_active() {
+ let env = Env::default();
+ let contract_id = env.register_contract(None, TestContract);
+ env.as_contract(&contract_id, || {
+ init(&env);
+ transition_to(&env, ProtocolState::Active);
+ assert_eq!(get_state(&env), ProtocolState::Active);
+ });
+ }
+
+ #[test]
+ fn full_lifecycle() {
+ let env = Env::default();
+ let contract_id = env.register_contract(None, TestContract);
+ env.as_contract(&contract_id, || {
+ init(&env);
+ transition_to(&env, ProtocolState::Active);
+ transition_to(&env, ProtocolState::Settled);
+ transition_to(&env, ProtocolState::Closed);
+ assert_eq!(get_state(&env), ProtocolState::Closed);
+ });
+ }
+
+ #[test]
+ #[should_panic]
+ fn invalid_pending_to_settled_rejected() {
+ let env = Env::default();
+ let contract_id = env.register_contract(None, TestContract);
+ env.as_contract(&contract_id, || {
+ init(&env);
+ transition_to(&env, ProtocolState::Settled);
+ });
+ }
+
+ #[test]
+ #[should_panic]
+ fn require_state_panics_on_mismatch() {
+ let env = Env::default();
+ let contract_id = env.register_contract(None, TestContract);
+ env.as_contract(&contract_id, || {
+ init(&env);
+ require_state(&env, ProtocolState::Active);
+ });
+ }
+}
diff --git a/engine-core/src/event_struct.rs b/engine-core/src/event_struct.rs
index eddfb89..0267a33 100644
--- a/engine-core/src/event_struct.rs
+++ b/engine-core/src/event_struct.rs
@@ -46,6 +46,8 @@ pub const MOD_TREASURY: u32 = 0x03;
pub const MOD_CB: u32 = 0x04;
pub const MOD_BURN: u32 = 0x05;
pub const MOD_RECOVERY: u32 = 0x06;
+pub const MOD_FEE: u32 = 0x07;
+pub const MOD_CORE: u32 = 0x08;
pub const MOD_FEE: u32 = 0x07;
@@ -76,6 +78,9 @@ pub struct CompactEvent {
pub value: u64,
pub const ACT_TRIGGERED: u32 = 0x0A << 8;
+pub const ACT_FEE: u32 = 0x0B << 8;
+pub const ACT_INIT: u32 = 0x0C << 8;
+pub const ACT_TRANSITION: u32 = 0x0D << 8;
pub const ACT_FEE: u32 = 0x0B << 8;
pub const ACT_UPGRADE: u32 = 0x0C << 8;
pub const ACT_UPDATE: u32 = 0x0D << 8;
diff --git a/engine-core/src/lib.rs b/engine-core/src/lib.rs
index 8ae9884..1b14ef7 100644
--- a/engine-core/src/lib.rs
+++ b/engine-core/src/lib.rs
@@ -17,6 +17,9 @@ pub mod treasury;
pub mod types;
pub mod upgrade;
pub mod version;
+pub mod event_struct;
+pub mod event_utils;
+pub mod core;
#[cfg(test)]
mod governance_tests;