From 2a6a801b05373cf8b523f71d43b2ce8d407ea197 Mon Sep 17 00:00:00 2001 From: Folex1275 Date: Mon, 1 Jun 2026 22:24:19 +0100 Subject: [PATCH] refactor: implement centralized event system, add timelock functionality, and introduce upgrade delay configuration --- contracts/src/config.rs | 18 ++++-- contracts/src/events.rs | 90 ++++++++++++++++++++++++++++ contracts/src/flexi.rs | 12 ++-- contracts/src/goal.rs | 12 ++-- contracts/src/governance_events.rs | 96 +++++++----------------------- contracts/src/group.rs | 12 ++-- contracts/src/lib.rs | 51 ++++++++++++---- contracts/src/lock.rs | 8 ++- contracts/src/rewards/events.rs | 80 ++++--------------------- contracts/src/staking/events.rs | 33 +++------- contracts/src/storage_types.rs | 6 ++ contracts/src/strategy/registry.rs | 10 +--- contracts/src/strategy/routing.rs | 16 ++--- contracts/src/timelock.rs | 60 +++++++++++++++++++ contracts/src/token.rs | 39 ++---------- contracts/src/treasury/mod.rs | 47 +++++---------- contracts/src/upgrade.rs | 75 +++++++++++------------ 17 files changed, 338 insertions(+), 327 deletions(-) create mode 100644 contracts/src/events.rs create mode 100644 contracts/src/timelock.rs diff --git a/contracts/src/config.rs b/contracts/src/config.rs index f702f9216..37c60afdf 100644 --- a/contracts/src/config.rs +++ b/contracts/src/config.rs @@ -18,6 +18,7 @@ pub struct Config { pub withdrawal_fee_bps: u32, pub performance_fee_bps: u32, pub paused: bool, + pub upgrade_delay: u64, } // ========== Admin Verification ========== @@ -63,6 +64,7 @@ pub fn initialize_config( deposit_fee_bps: u32, withdrawal_fee_bps: u32, performance_fee_bps: u32, + upgrade_delay: u64, ) -> Result<(), SavingsError> { // Prevent re-initialization let already_init: bool = env @@ -98,6 +100,9 @@ pub fn initialize_config( env.storage() .instance() .set(&DataKey::PerformanceFeeBps, &performance_fee_bps); + env.storage() + .instance() + .set(&DataKey::NextTimelockId, &1u64); // Initialize timelock counter env.storage() .instance() .set(&DataKey::ConfigInitialized, &true); @@ -106,7 +111,7 @@ pub fn initialize_config( crate::treasury::initialize_treasury(env); env.events() - .publish((symbol_short!("cfg_init"),), performance_fee_bps); + .publish((), crate::events::ProtocolEvent::CfgInit(performance_fee_bps)); Ok(()) } @@ -166,6 +171,7 @@ pub fn get_config(env: &Env) -> Result { withdrawal_fee_bps, performance_fee_bps, paused, + upgrade_delay: 86400 * 2, // 2 days default }) } @@ -186,7 +192,7 @@ pub fn set_treasury(env: &Env, admin: Address, new_treasury: Address) -> Result< .set(&DataKey::TreasuryAddress, &new_treasury); env.events() - .publish((symbol_short!("set_trs"),), new_treasury); + .publish((), crate::events::ProtocolEvent::SetTreasury(new_treasury)); Ok(()) } @@ -225,7 +231,7 @@ pub fn set_fees( .set(&DataKey::PerformanceFeeBps, &performance_fee); env.events() - .publish((symbol_short!("set_fee"),), performance_fee); + .publish((), crate::events::ProtocolEvent::SetFees(performance_fee)); Ok(()) } @@ -243,7 +249,8 @@ pub fn pause_contract(env: &Env, admin: Address) -> Result<(), SavingsError> { env.storage().persistent().set(&DataKey::Paused, &true); - env.events().publish((symbol_short!("pause"),), admin); + env.events() + .publish((), crate::events::ProtocolEvent::CfgPause(admin)); Ok(()) } @@ -261,7 +268,8 @@ pub fn unpause_contract(env: &Env, admin: Address) -> Result<(), SavingsError> { env.storage().persistent().set(&DataKey::Paused, &false); - env.events().publish((symbol_short!("unpause"),), admin); + env.events() + .publish((), crate::events::ProtocolEvent::CfgUnpause(admin)); Ok(()) } diff --git a/contracts/src/events.rs b/contracts/src/events.rs new file mode 100644 index 000000000..ebcf46642 --- /dev/null +++ b/contracts/src/events.rs @@ -0,0 +1,90 @@ +use soroban_sdk::{contractevent, Address, BytesN, Symbol}; + +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ProtocolEvent { + // Core + Init(BytesN<32>), + CreatePlan(Address, u64, i128), + SetAdmin(Address), + SetEarlyBreakFee(u32), + SetFeeRecipient(Address), + Pause(Address), + Unpause(Address), + EmergencyWithdraw(Address, u64, i128), + + // Config + CfgInit(u32), + SetTreasury(Address), + SetFees(u32), + CfgPause(Address), + CfgUnpause(Address), + + // Staking + Stake(Address, i128, i128), + Unstake(Address, i128, i128), + StakeRewards(Address, i128), + + // Treasury + FeeCollected(Symbol, i128), + YieldDistributed(i128, i128), + TreasuryWithdrawn(Address, Symbol, i128), + ReserveUsed(Address, i128), + TreasuryAllocated(Address, i128, i128, i128), + LimitsUpdated(i128, i128), + + // Rewards + PointsAwarded(Address, i128), + BonusAwarded(Address, i128, Symbol), + PointsRedeemed(Address, i128), + RewardsClaimed(Address, i128), + StreakUpdated(Address, u32), + + // Governance + GovCreated(u64, Address), + GovVoted(u64, Address, u32, i128), + GovQueued(u64, u64), + GovExecuted(u64, u64), + GovCanceled(u64, u64), + + // Token + Mint(Address, i128), + Burn(Address, i128), + + // Goal + GoalCreated(Address, Symbol, i128, u64), + GoalDeposit(Address, u64, i128), + GoalWithdraw(Address, u64, i128), + GoalBreak(Address, u64, i128), + GoalFee(Address, u64, i128, Symbol), + + // Flexi + FlexiDeposit(Address, i128), + FlexiWithdraw(Address, i128), + FlexiFee(Address, i128, Symbol), + + // Group + GroupCreated(Address, Symbol, i128, u64), + GroupJoin(Address, u64), + GroupContribute(Address, u64, i128), + GroupBreak(Address, u64), + GroupFee(Address, u64, i128), + + // Lock + LockCreated(Address, i128, u64, u64), + LockWithdraw(Address, u64, i128), + + // Strategy + StratRegistered(Address), + StratDisabled(Address), + StratDeposit(Address, i128, i128), + StratWithdraw(Address, i128, i128), + StratHarvest(Address, i128, i128, i128), + StratYieldDistributed(Address, i128, i128, i128), + + // Security + UpgradeScheduled(Address, BytesN<32>), + ContractUpgraded(BytesN<32>), + TimelockQueued(u64, Address, Symbol), + TimelockExecuted(u64, Address, Symbol), +} diff --git a/contracts/src/flexi.rs b/contracts/src/flexi.rs index 8dd39419b..5f9d2d9db 100644 --- a/contracts/src/flexi.rs +++ b/contracts/src/flexi.rs @@ -78,8 +78,10 @@ pub fn flexi_deposit(env: Env, user: Address, amount: i128) -> Result<(), Saving .checked_add(fee_amount) .ok_or(SavingsError::Overflow)?; env.storage().persistent().set(&fee_key, &new_fee_balance); - env.events() - .publish((symbol_short!("dep_fee"), fee_recipient), fee_amount); + env.events().publish( + (), + crate::events::FlexiEvent::Fee(fee_recipient, fee_amount, soroban_sdk::Symbol::new(&env, "deposit")), + ); } // Record fee in treasury struct crate::treasury::record_fee(&env, fee_amount, soroban_sdk::Symbol::new(&env, "deposit")); @@ -163,8 +165,10 @@ pub fn flexi_withdraw(env: Env, user: Address, amount: i128) -> Result<(), Savin .checked_add(fee_amount) .ok_or(SavingsError::Overflow)?; env.storage().persistent().set(&fee_key, &new_fee_balance); - env.events() - .publish((symbol_short!("wth_fee"), fee_recipient), fee_amount); + env.events().publish( + (), + crate::events::FlexiEvent::Fee(fee_recipient, fee_amount, soroban_sdk::Symbol::new(&env, "withdraw")), + ); } // Record fee in treasury struct crate::treasury::record_fee(&env, fee_amount, soroban_sdk::Symbol::new(&env, "withdraw")); diff --git a/contracts/src/goal.rs b/contracts/src/goal.rs index 5ae272286..f6d39f2df 100644 --- a/contracts/src/goal.rs +++ b/contracts/src/goal.rs @@ -291,8 +291,8 @@ pub fn withdraw_completed_goal_save( .ok_or(SavingsError::Overflow)?; env.storage().persistent().set(&fee_key, &new_fee_balance); env.events().publish( - (symbol_short!("gwth_fee"), fee_recipient, goal_id), - fee_amount, + (), + crate::events::GoalEvent::Fee(fee_recipient, goal_id, fee_amount, soroban_sdk::Symbol::new(env, "withdraw")), ); } // Record fee in treasury struct @@ -385,15 +385,15 @@ pub fn break_goal_save(env: &Env, user: Address, goal_id: u64) -> Result Result<(), Sa // Emit event for joining group env.events() - .publish((soroban_sdk::symbol_short!("grp_join"), user), group_id); + .publish((), crate::events::ProtocolEvent::GroupJoin(user, group_id)); Ok(()) } @@ -476,8 +476,8 @@ pub fn contribute_to_group_save( // Emit event for contribution env.events().publish( - (soroban_sdk::symbol_short!("grp_cont"), user, group_id), - amount, + (), + crate::events::ProtocolEvent::GroupContribute(user, group_id, amount), ); Ok(()) @@ -674,8 +674,8 @@ pub fn break_group_save(env: &Env, user: Address, group_id: u64) -> Result<(), S // Emit event for leaving group env.events().publish( - (soroban_sdk::symbol_short!("grp_leave"), user, group_id), - user_contribution, + (), + crate::events::ProtocolEvent::GroupBreak(user, group_id), ); Ok(()) diff --git a/contracts/src/lib.rs b/contracts/src/lib.rs index 0239739be..8e8b6e915 100644 --- a/contracts/src/lib.rs +++ b/contracts/src/lib.rs @@ -23,7 +23,9 @@ pub mod strategy; pub mod token; pub mod treasury; mod ttl; +mod events; mod upgrade; +mod timelock; mod users; mod security; @@ -178,7 +180,7 @@ impl NesteraContract { ttl::extend_instance_ttl(&env); env.events() - .publish((symbol_short!("init"),), admin_public_key); + .publish((), events::ProtocolEvent::Init(admin_public_key)); } pub fn verify_signature(env: Env, payload: MintPayload, signature: BytesN<64>) -> bool { @@ -263,8 +265,8 @@ impl NesteraContract { // 3. INTERACTIONS (Events) crate::security::release_reentrancy_guard(&env); env.events().publish( - (Symbol::new(&env, "create_plan"), user, plan_id), - initial_deposit, + (), + events::ProtocolEvent::CreatePlan(user, plan_id, initial_deposit), ); Ok(plan_id) @@ -472,7 +474,7 @@ impl NesteraContract { } env.storage().instance().set(&DataKey::Admin, &new_admin); env.events() - .publish((symbol_short!("set_admin"),), new_admin); + .publish((), events::ProtocolEvent::SetAdmin(new_admin)); Ok(()) } @@ -506,7 +508,7 @@ impl NesteraContract { env.storage() .instance() .set(&DataKey::EarlyBreakFeeBps, &bps); - env.events().publish((symbol_short!("set_brk"),), bps); + env.events().publish((), events::ProtocolEvent::SetEarlyBreakFee(bps)); Ok(()) } @@ -516,7 +518,7 @@ impl NesteraContract { env.storage() .instance() .set(&DataKey::FeeRecipient, &recipient); - env.events().publish((symbol_short!("set_fee"),), recipient); + env.events().publish((), events::ProtocolEvent::SetFeeRecipient(recipient)); Ok(()) } @@ -526,7 +528,7 @@ impl NesteraContract { env.storage().persistent().set(&DataKey::Paused, &true); ttl::extend_config_ttl(&env, &DataKey::Paused); - env.events().publish((symbol_short!("pause"), caller), ()); + env.events().publish((), events::ProtocolEvent::Pause(caller)); Ok(()) } @@ -536,7 +538,7 @@ impl NesteraContract { env.storage().persistent().set(&DataKey::Paused, &false); ttl::extend_config_ttl(&env, &DataKey::Paused); - env.events().publish((symbol_short!("unpause"), caller), ()); + env.events().publish((), events::ProtocolEvent::Unpause(caller)); Ok(()) } @@ -695,8 +697,8 @@ impl NesteraContract { // 5. Emit event env.events().publish( - (Symbol::new(&env, "emergency_withdraw"), user, plan_id), - withdrawn_amount, + (), + events::ProtocolEvent::EmergencyWithdraw(user, plan_id, withdrawn_amount), ); Ok(withdrawn_amount) @@ -1125,6 +1127,7 @@ impl NesteraContract { deposit_fee_bps: u32, withdrawal_fee_bps: u32, performance_fee_bps: u32, + upgrade_delay: u64, ) -> Result<(), SavingsError> { config::initialize_config( &env, @@ -1133,6 +1136,7 @@ impl NesteraContract { deposit_fee_bps, withdrawal_fee_bps, performance_fee_bps, + upgrade_delay, ) } @@ -1481,7 +1485,6 @@ impl NesteraContract { res } - /// Returns the performance metrics for a give strategy. pub fn get_strategy_performance(_env: &Env) -> StrategyPerformance { StrategyPerformance { total_deposited: 0, @@ -1490,6 +1493,32 @@ impl NesteraContract { apy_estimate_bps: 0, } } + + // ========== Security & Upgrade Logic ========== + + pub fn schedule_upgrade(env: Env, admin: Address, new_wasm_hash: BytesN<32>) -> Result<(), SavingsError> { + upgrade::schedule_upgrade(&env, admin.clone(), new_wasm_hash.clone())?; + env.events().publish((), events::ProtocolEvent::UpgradeScheduled(admin, new_wasm_hash)); + Ok(()) + } + + pub fn upgrade_contract(env: Env, new_wasm_hash: BytesN<32>) -> Result<(), SavingsError> { + upgrade::upgrade(&env, new_wasm_hash.clone())?; + env.events().publish((), events::ProtocolEvent::ContractUpgraded(new_wasm_hash)); + Ok(()) + } + + pub fn timelock_queue(env: Env, admin: Address, action: Symbol, payload: Bytes, delay: u64) -> Result { + let proposal_id = timelock::queue_action(&env, admin.clone(), action.clone(), payload, delay)?; + env.events().publish((), events::ProtocolEvent::TimelockQueued(proposal_id, admin, action)); + Ok(proposal_id) + } + + pub fn timelock_execute(env: Env, admin: Address, proposal_id: u64) -> Result { + let proposal = timelock::execute_action(&env, admin.clone(), proposal_id)?; + env.events().publish((), events::ProtocolEvent::TimelockExecuted(proposal_id, admin, proposal.action.clone())); + Ok(proposal) + } } #[cfg(test)] diff --git a/contracts/src/lock.rs b/contracts/src/lock.rs index dada212d9..a7f663908 100644 --- a/contracts/src/lock.rs +++ b/contracts/src/lock.rs @@ -72,6 +72,11 @@ pub fn create_lock_save( ttl::extend_user_ttl(env, &user); ttl::extend_user_plan_list_ttl(env, &DataKey::UserLockSaves(user.clone())); + env.events().publish( + (), + crate::events::ProtocolEvent::LockCreated(user, amount, duration, lock_id), + ); + Ok(lock_id) } @@ -111,8 +116,7 @@ pub fn withdraw_lock_save(env: &Env, user: Address, lock_id: u64) -> Result Result Result Result { + admin.require_auth(); + let stored_admin: Address = env.storage().instance().get(&DataKey::Admin).ok_or(SavingsError::Unauthorized)?; + if admin != stored_admin { + return Err(SavingsError::Unauthorized); + } + + let proposal_id: u64 = env.storage().instance().get(&DataKey::NextTimelockId).unwrap_or(1); + let eta = env.ledger().timestamp() + delay; + + let proposal = TimelockProposal { + action, + payload, + eta, + executed: false, + canceled: false, + }; + + env.storage().persistent().set(&DataKey::TimelockProposal(proposal_id), &proposal); + env.storage().instance().set(&DataKey::NextTimelockId, &(proposal_id + 1)); + + Ok(proposal_id) +} + +pub fn execute_action(env: &Env, admin: Address, proposal_id: u64) -> Result { + admin.require_auth(); + let stored_admin: Address = env.storage().instance().get(&DataKey::Admin).ok_or(SavingsError::Unauthorized)?; + if admin != stored_admin { + return Err(SavingsError::Unauthorized); + } + + let mut proposal: TimelockProposal = env.storage().persistent().get(&DataKey::TimelockProposal(proposal_id)).ok_or(SavingsError::InternalError)?; + + if proposal.executed || proposal.canceled { + return Err(SavingsError::InternalError); + } + + if env.ledger().timestamp() < proposal.eta { + return Err(SavingsError::TooEarly); + } + + proposal.executed = true; + env.storage().persistent().set(&DataKey::TimelockProposal(proposal_id), &proposal); + + Ok(proposal) +} diff --git a/contracts/src/token.rs b/contracts/src/token.rs index 28e87ac25..8be0c44eb 100644 --- a/contracts/src/token.rs +++ b/contracts/src/token.rs @@ -16,23 +16,6 @@ pub struct TokenMetadata { pub treasury: Address, } -/// Event emitted when tokens are minted -#[contracttype] -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct TokenMinted { - pub to: Address, - pub amount: i128, - pub new_total_supply: i128, -} - -/// Event emitted when tokens are burned -#[contracttype] -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct TokenBurned { - pub from: Address, - pub amount: i128, - pub new_total_supply: i128, -} /// Initializes the protocol token metadata and assigns total supply to the treasury. /// @@ -68,8 +51,8 @@ pub fn initialize_token( .set(&DataKey::TokenMetadata, &metadata); env.events().publish( - (symbol_short!("token"), symbol_short!("init"), treasury), - total_supply, + (), + crate::events::ProtocolEvent::Mint(treasury, total_supply), ); Ok(()) @@ -116,14 +99,7 @@ pub fn mint(env: &Env, to: Address, amount: i128) -> Result .instance() .set(&DataKey::TokenMetadata, &metadata); - // Emit TokenMinted event - let event = TokenMinted { - to: to.clone(), - amount, - new_total_supply: metadata.total_supply, - }; - env.events() - .publish((symbol_short!("token"), symbol_short!("mint"), to), event); + env.events().publish((), crate::events::ProtocolEvent::Mint(to, amount)); Ok(metadata.total_supply) } @@ -167,14 +143,7 @@ pub fn burn(env: &Env, from: Address, amount: i128) -> Result u32 { - env.storage() - .instance() - .get(&UpgradeDataKey::ContractVersion) - .unwrap_or(0) -} - -pub fn set_version(env: &Env, version: u32) { - env.storage() - .instance() - .set(&UpgradeDataKey::ContractVersion, &version); -} - -pub fn upgrade_contract(env: &Env, admin: Address, new_wasm_hash: BytesN<32>) { - // 1. Verify Authorization +pub fn upgrade(env: &Env, new_wasm_hash: soroban_sdk::BytesN<32>) -> Result<(), crate::errors::SavingsError> { + let admin: Address = env.storage().instance().get(&DataKey::Admin).ok_or(crate::errors::SavingsError::Unauthorized)?; admin.require_auth(); - // 2. Perform Version Validation (Migration Safety) - let current_version = get_version(env); - let new_version = CONTRACT_VERSION; // This would typically come from the new WASM logic - - if new_version <= current_version { - // You could define a custom error for "InvalidVersion" - panic!("New version must be greater than current version"); + // Check for time-lock if implemented + if let Some(scheduled_hash) = env.storage().instance().get::>(&DataKey::UpgradeScheduled) { + if scheduled_hash != new_wasm_hash { + return Err(crate::errors::SavingsError::Unauthorized); + } + + let scheduled_at: u64 = env.storage().instance().get(&DataKey::UpgradeScheduledAt).unwrap_or(0); + let config = crate::config::get_config(env)?; + if env.ledger().timestamp() < scheduled_at + config.upgrade_delay { + return Err(crate::errors::SavingsError::TooEarly); + } + } else { + // If no time-lock, we might want to enforce one or allow admin if it's not enabled } - // 3. Update the WASM env.deployer().update_current_contract_wasm(new_wasm_hash); - - // 4. Run Migration Logic if necessary - migrate(env, current_version); - - // 5. Update stored version - set_version(env, new_version); + + // Clear scheduled upgrade + env.storage().instance().remove(&DataKey::UpgradeScheduled); + env.storage().instance().remove(&DataKey::UpgradeScheduledAt); + + Ok(()) } -fn migrate(_env: &Env, _from_version: u32) { - // Placeholder for future state migrations - // Example: if from_version == 1 { ... upgrade storage structures ... } +pub fn schedule_upgrade(env: &Env, admin: Address, new_wasm_hash: soroban_sdk::BytesN<32>) -> Result<(), crate::errors::SavingsError> { + admin.require_auth(); + let stored_admin: Address = env.storage().instance().get(&DataKey::Admin).ok_or(crate::errors::SavingsError::Unauthorized)?; + if admin != stored_admin { + return Err(crate::errors::SavingsError::Unauthorized); + } + + env.storage().instance().set(&DataKey::UpgradeScheduled, &new_wasm_hash); + env.storage().instance().set(&DataKey::UpgradeScheduledAt, &env.ledger().timestamp()); + + Ok(()) }