diff --git a/push_changes.bat b/push_changes.bat new file mode 100644 index 0000000..a913983 --- /dev/null +++ b/push_changes.bat @@ -0,0 +1,19 @@ +@echo off +cd /d "c:\Users\hp\Desktop\wave7\stellarflow-contracts-1" + +:: Create and checkout new branch +git checkout -b feature/dynamic-fees + +:: Add all modified files +git add src/fees.rs src/lib.rs src/amm/invariant.rs + +:: Commit changes +git commit -m "Implement dynamic trading fee adjustment (0.05%%-0.30%%) based on pool volume shifts + +- Add VolumeHistory and DynamicFeeState structs to track volume and fees +- Implement automatic fee adjustment based on volume delta (>50%% increase, >30%% decrease) +- Integrate dynamic fee deduction with swap output calculation +- Add admin configuration for fee parameters" + +:: Push branch (you may need to specify your remote name, usually 'origin') +echo "Branch created and committed. To push, run: git push -u origin feature/dynamic-fees" \ No newline at end of file diff --git a/src/amm/invariant.rs b/src/amm/invariant.rs index 5b96ac5..eb84cb8 100644 --- a/src/amm/invariant.rs +++ b/src/amm/invariant.rs @@ -74,24 +74,41 @@ fn mul_div(numerator: u128, denominator: u128, divisor: u128) -> Result Result { +) -> Result<(u128, u128), ContractError> { if amount_in == 0 || reserve_in == 0 || reserve_out == 0 { return Err(ContractError::InvalidInput); } + + // Update volume history and get current dynamic fee + let fee_bps = crate::TimeLockedUpgradeContract::update_volume_and_get_fee( + env, + asset, + amount_in as u64 + )?; + + // Calculate raw output before fees let denominator = reserve_in .checked_add(amount_in) .ok_or(ContractError::Overflow)?; - mul_div(reserve_out, amount_in, denominator) + let raw_output = mul_div(reserve_out, amount_in, denominator)?; + + // Apply dynamic fee deduction + let (amount_after_fees, fee_amount) = crate::TimeLockedUpgradeContract::calculate_and_deduct_fee( + raw_output, + fee_bps + )?; + + Ok((amount_after_fees, fee_amount)) } /// Compute the amount of LP shares to mint for a liquidity deposit. @@ -331,4 +348,4 @@ mod tests { assert!(amount_out > 0); assert_invariant_stable(reserve_in, reserve_out, amount_in, amount_out).unwrap(); } -} +} \ No newline at end of file diff --git a/src/fees.rs b/src/fees.rs index a448d08..4440840 100644 --- a/src/fees.rs +++ b/src/fees.rs @@ -31,6 +31,48 @@ pub struct CorridorFeePool { #[contracttype] pub enum FeesStorageKey { CorridorPool(AssetId), + VolumeHistory(AssetId), + DynamicFee(AssetId), +} + +/// Historical volume tracking to calculate volume delta +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct VolumeHistory { + pub previous_period_volume: u64, + pub current_period_volume: u64, + pub last_updated: u64, // timestamp when period was last rotated +} + +impl VolumeHistory { + fn new() -> Self { + Self { + previous_period_volume: 0, + current_period_volume: 0, + last_updated: 0, + } + } +} + +/// Dynamic fee configuration and current state +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct DynamicFeeState { + pub min_fee_bps: u32, // 5 = 0.05% + pub max_fee_bps: u32, // 30 = 0.30% + pub current_fee_bps: u32, + pub period_seconds: u64, // how often to recalculate (default: 3600 = 1 hour) +} + +impl DynamicFeeState { + fn new() -> Self { + Self { + min_fee_bps: 5, // 0.05% + max_fee_bps: 30, // 0.30% + current_fee_bps: 5, // start at minimum + period_seconds: 3600, // 1 hour recalculation period + } + } } impl CorridorFeePool { @@ -204,6 +246,136 @@ pub fn get_corridor_fee_pool(env: Env, asset: AssetId) -> CorridorFeePool { .unwrap_or(CorridorFeePool::new(asset)) } +/// Update volume history and recalculate dynamic fee if period has elapsed +pub fn update_volume_and_adjust_fee(env: &Env, asset: AssetId, trade_volume: u64) -> Result { + let volume_key = FeesStorageKey::VolumeHistory(asset.clone()); + let fee_key = FeesStorageKey::DynamicFee(asset.clone()); + + let mut volume_history: VolumeHistory = env.storage() + .instance() + .get(&volume_key) + .unwrap_or(VolumeHistory::new()); + + let mut dynamic_fee: DynamicFeeState = env.storage() + .instance() + .get(&fee_key) + .unwrap_or(DynamicFeeState::new()); + + let current_timestamp = env.ledger().timestamp(); + + // Check if we need to rotate to a new period + if current_timestamp >= volume_history.last_updated + dynamic_fee.period_seconds { + // Move current volume to previous, reset current + volume_history.previous_period_volume = volume_history.current_period_volume; + volume_history.current_period_volume = trade_volume; + volume_history.last_updated = current_timestamp; + + // Calculate volume delta and adjust fee + let new_fee = calculate_dynamic_fee(&volume_history, &dynamic_fee)?; + dynamic_fee.current_fee_bps = new_fee; + } else { + // Still in the same period, just add to current volume + volume_history.current_period_volume = volume_history.current_period_volume + .checked_add(trade_volume) + .ok_or(ContractError::MathOverflow)?; + } + + // Save updated state + env.storage().instance().set(&volume_key, &volume_history); + env.storage().instance().set(&fee_key, &dynamic_fee); + + Ok(dynamic_fee.current_fee_bps) +} + +/// Calculate volume delta between periods and adjust fee within bounds +fn calculate_dynamic_fee(volume_history: &VolumeHistory, dynamic_fee: &DynamicFeeState) -> Result { + // If no previous volume, keep current fee + if volume_history.previous_period_volume == 0 { + return Ok(dynamic_fee.current_fee_bps); + } + + // Calculate volume change ratio (current / previous) + let volume_delta = volume_history.current_period_volume as f64 / volume_history.previous_period_volume as f64; + + // Adjust fee based on volume changes: + // - Volume spiked > 50%: increase fee to reduce congestion + // - Volume dropped > 30%: decrease fee to attract more trading + let new_fee_bps = if volume_delta > 1.5 { + // Volume increased significantly - raise fee + dynamic_fee.current_fee_bps.saturating_add(5) + } else if volume_delta < 0.7 { + // Volume decreased significantly - lower fee + dynamic_fee.current_fee_bps.saturating_sub(5) + } else { + // No significant change - keep current fee + dynamic_fee.current_fee_bps + }; + + // Clamp fee to within allowed range [0.05%, 0.30%] = [5bps, 30bps] + Ok(new_fee_bps.clamp(dynamic_fee.min_fee_bps, dynamic_fee.max_fee_bps)) +} + +/// Get the current dynamic fee for an asset +pub fn get_current_dynamic_fee(env: &Env, asset: AssetId) -> u32 { + let fee_key = FeesStorageKey::DynamicFee(asset); + let dynamic_fee: DynamicFeeState = env.storage() + .instance() + .get(&fee_key) + .unwrap_or(DynamicFeeState::new()); + dynamic_fee.current_fee_bps +} + +/// Calculate and deduct dynamic fee from a trade amount +pub fn calculate_and_deduct_fee(amount: u128, fee_bps: u32) -> Result<(u128, u128), ContractError> { + // Fee is calculated as (amount * fee_bps) / 10000 (since bps is 1/100th of a percent) + let fee_amount = amount + .checked_mul(fee_bps as u128) + .ok_or(ContractError::Overflow)? + .checked_div(10000) + .ok_or(ContractError::DivisionByZero)?; + + let amount_after_fees = amount + .checked_sub(fee_amount) + .ok_or(ContractError::MathOverflow)?; + + Ok((amount_after_fees, fee_amount)) +} + +/// Admin function to update dynamic fee configuration +pub fn set_dynamic_fee_config( + env: &Env, + caller: &Address, + asset: AssetId, + min_fee_bps: u32, + max_fee_bps: u32, + period_seconds: u64, +) -> Result<(), ContractError> { + use crate::auth::_require_authorized; + _require_authorized(env, caller); + + // Validate bounds + if min_fee_bps < 5 || max_fee_bps > 30 || min_fee_bps >= max_fee_bps { + return Err(ContractError::InvalidVarianceConfig); + } + if period_seconds < 300 { // Minimum 5 minutes to prevent excessive recalculations + return Err(ContractError::InvalidVarianceConfig); + } + + let fee_key = FeesStorageKey::DynamicFee(asset); + let mut dynamic_fee: DynamicFeeState = env.storage() + .instance() + .get(&fee_key) + .unwrap_or(DynamicFeeState::new()); + + dynamic_fee.min_fee_bps = min_fee_bps; + dynamic_fee.max_fee_bps = max_fee_bps; + dynamic_fee.period_seconds = period_seconds; + + env.storage().instance().set(&fee_key, &dynamic_fee); + + Ok(()) +} + // --------------------------------------------------------------------------- // Corridor weight profile functions — independent access control (issue #530) // --------------------------------------------------------------------------- @@ -427,4 +599,4 @@ mod tests { Err(ContractError::Overflow) ); } -} +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index c4c7bcb..33fb40b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -693,8 +693,35 @@ impl TimeLockedUpgradeContract { Ok(pool) } - pub fn get_corridor_fee_pool(env: Env, asset: AssetId) -> fees::CorridorFeePool { - fees::get_corridor_fee_pool(env, asset) + pub fn get_corridor_fee_pool(env: Env, asset: AssetId) -> CorridorFeePool { + crate::fees::get_corridor_fee_pool(env, asset) + } + + /// Get the current dynamic trading fee for an asset (in basis points) + pub fn get_current_dynamic_fee(env: Env, asset: AssetId) -> u32 { + crate::fees::get_current_dynamic_fee(&env, asset) + } + + /// Admin function to configure dynamic fee parameters + pub fn set_dynamic_fee_config( + env: Env, + caller: Address, + asset: AssetId, + min_fee_bps: u32, + max_fee_bps: u32, + period_seconds: u64, + ) -> Result<(), ContractError> { + crate::fees::set_dynamic_fee_config(&env, &caller, asset, min_fee_bps, max_fee_bps, period_seconds) + } + + /// Update volume history and get the current dynamic fee (called internally during swaps) + pub(crate) fn update_volume_and_get_fee(env: &Env, asset: AssetId, trade_volume: u64) -> Result { + crate::fees::update_volume_and_adjust_fee(env, asset, trade_volume) + } + + /// Calculate and deduct the dynamic fee from a trade amount + pub(crate) fn calculate_and_deduct_fee(amount: u128, fee_bps: u32) -> Result<(u128, u128), ContractError> { + crate::fees::calculate_and_deduct_fee(amount, fee_bps) } pub fn set_corridor_weight( @@ -1388,4 +1415,4 @@ mod query_guardrail_tests { // Integration tests for issue #525 live in `src/slashing.rs`. #[cfg(test)] -mod test; +mod test; \ No newline at end of file