@@ -6,7 +6,7 @@ use crate::escrow;
66use crate :: market;
77use crate :: storage_types:: {
88 DataKey , FeeTier , FeeTierConfig , LPPosition , LiquidityPool , Market , MarketFeeInfo ,
9- PriceAccumulator , PriceObservation , SwapRecord , VolatilityState ,
9+ PriceAccumulator , PriceObservation , SwapRecord , VolatilityState , VolumeFeeConfig ,
1010} ;
1111
1212// ── Constants ─────────────────────────────────────────────────────────────────
@@ -142,6 +142,29 @@ pub fn fee_bps_for_tier(tier: &FeeTier, cfg: &FeeTierConfig) -> u32 {
142142 }
143143}
144144
145+ /// Select the volume-based fee tier for a market given its cumulative volume.
146+ /// Returns the index into `VolumeFeeConfig::tiers` and the corresponding fee bps.
147+ /// The last tier whose threshold is ≤ `cumulative_volume` is chosen.
148+ pub fn select_volume_fee_tier (
149+ cumulative_volume : i128 ,
150+ config : & VolumeFeeConfig ,
151+ ) -> ( u32 , u32 ) {
152+ let mut active_idx: u32 = 0 ;
153+ let mut active_fee_bps = config. tiers . get ( 0 ) . map ( |t| t. fee_bps ) . unwrap_or ( 30 ) ;
154+
155+ for i in 1 ..config. tiers . len ( ) {
156+ let entry = config. tiers . get ( i) . unwrap ( ) ;
157+ if cumulative_volume >= entry. volume_threshold {
158+ active_idx = i;
159+ active_fee_bps = entry. fee_bps ;
160+ } else {
161+ break ;
162+ }
163+ }
164+
165+ ( active_idx, active_fee_bps)
166+ }
167+
145168fn validate_fee_tier_config ( cfg : & FeeTierConfig ) -> Result < ( ) , InsightArenaError > {
146169 if cfg. calm_threshold_bps >= cfg. volatile_threshold_bps {
147170 return Err ( InsightArenaError :: InvalidInput ) ;
@@ -265,20 +288,28 @@ fn update_volatility_state(
265288 Ok ( state)
266289}
267290
268- /// Return the current dynamic fee tier and effective swap fee for a market.
291+ /// Return the current dynamic fee state for a market.
292+ /// `effective_fee_bps` reflects the volume-based fee tier active for this
293+ /// market's cumulative volume. Volatility-tier info (`tier`, `volatility_ema_bps`)
294+ /// is provided for informational / off-chain analysis.
269295pub fn get_market_fee_info ( env : & Env , market_id : u64 ) -> Result < MarketFeeInfo , InsightArenaError > {
270- market:: get_market ( env, market_id) ?;
296+ let mkt = market:: get_market ( env, market_id) ?;
271297
272298 let tier_config = get_fee_tier_config ( env) ;
273299 let volatility = get_volatility_state ( env, market_id) ;
274300 let tier = determine_fee_tier ( volatility. ema_bps , & tier_config) ;
275- let effective_fee_bps = fee_bps_for_tier ( & tier, & tier_config) ;
301+
302+ let cfg = config:: get_config ( env) ?;
303+ let ( volume_tier_index, effective_fee_bps) =
304+ select_volume_fee_tier ( mkt. cumulative_volume , & cfg. volume_fee_config ) ;
276305
277306 Ok ( MarketFeeInfo {
278307 market_id,
279308 tier,
280309 effective_fee_bps,
281310 volatility_ema_bps : volatility. ema_bps ,
311+ volume_tier_index,
312+ volume_tier_fee_bps : effective_fee_bps,
282313 } )
283314}
284315
@@ -962,12 +993,17 @@ pub fn swap_outcome(
962993 . get ( to_outcome. clone ( ) )
963994 . ok_or ( InsightArenaError :: InvalidOutcome ) ?;
964995
965- // Fee tier is derived from volatility observed *before* this swap, so a
966- // trade cannot influence the fee rate it itself pays.
996+ // ── Volume-based fee tier selection ────────────────────────────────────
997+ // The fee is derived from the market's cumulative volume *before* this
998+ // swap, so a trade cannot influence the fee rate it itself pays.
999+ let cfg = config:: get_config ( env) ?;
1000+ let volume_before = mkt. cumulative_volume ;
1001+ let ( volume_tier_before, effective_fee_bps) =
1002+ select_volume_fee_tier ( volume_before, & cfg. volume_fee_config ) ;
1003+
1004+ // Volatility state is still tracked (for informational purposes / TWAP).
9671005 let tier_config = get_fee_tier_config ( env) ;
9681006 let volatility_before = get_volatility_state ( env, market_id) ;
969- let tier = determine_fee_tier ( volatility_before. ema_bps , & tier_config) ;
970- let effective_fee_bps = fee_bps_for_tier ( & tier, & tier_config) ;
9711007
9721008 let amount_out = calculate_swap_output ( amount_in, from_reserve, to_reserve, effective_fee_bps) ?;
9731009
@@ -984,6 +1020,7 @@ pub fn swap_outcome(
9841020 // Split the fee between the protocol treasury and liquidity providers.
9851021 // `lp_fee_share` is derived by subtraction so the two shares always sum
9861022 // to `fee_amount` exactly, with no stroop lost or double-counted.
1023+ // Protocol share bps is read from the volatility-based FeeTierConfig.
9871024 let protocol_fee_share = fee_amount
9881025 . checked_mul ( tier_config. protocol_share_bps as i128 )
9891026 . ok_or ( InsightArenaError :: Overflow ) ?
@@ -1027,7 +1064,6 @@ pub fn swap_outcome(
10271064 // default `treasury_split_bps == 10_000`, so the entire protocol fee
10281065 // share keeps flowing to the treasury exactly as it did before this
10291066 // split was introduced.
1030- let cfg = config:: get_config ( env) ?;
10311067 let treasury_amount = protocol_fee_share
10321068 . checked_mul ( cfg. treasury_split_bps as i128 )
10331069 . ok_or ( InsightArenaError :: Overflow ) ?
@@ -1054,6 +1090,26 @@ pub fn swap_outcome(
10541090 total_lp_share,
10551091 ) ;
10561092
1093+ // ── Update cumulative market volume and detect tier crossing ─────────────
1094+ let new_volume = volume_before
1095+ . checked_add ( amount_in)
1096+ . ok_or ( InsightArenaError :: Overflow ) ?;
1097+
1098+ let ( volume_tier_after, _) =
1099+ select_volume_fee_tier ( new_volume, & cfg. volume_fee_config ) ;
1100+
1101+ if volume_tier_after > volume_tier_before {
1102+ emit_volume_tier_crossed ( env, market_id, volume_tier_before, volume_tier_after, new_volume) ;
1103+ }
1104+
1105+ let mut mkt = mkt;
1106+ mkt. cumulative_volume = new_volume;
1107+ env. storage ( )
1108+ . persistent ( )
1109+ . set ( & DataKey :: Market ( market_id) , & mkt) ;
1110+ market:: bump_market ( env, market_id) ;
1111+
1112+ // ── Record swap with volume tier snapshot ────────────────────────────────
10571113 let record = SwapRecord :: new (
10581114 trader,
10591115 market_id,
@@ -1063,6 +1119,7 @@ pub fn swap_outcome(
10631119 amount_out,
10641120 fee_amount,
10651121 env. ledger ( ) . timestamp ( ) ,
1122+ volume_tier_before,
10661123 ) ;
10671124
10681125 let mut history: Vec < SwapRecord > = env
@@ -1080,6 +1137,19 @@ pub fn swap_outcome(
10801137 Ok ( amount_out)
10811138}
10821139
1140+ fn emit_volume_tier_crossed (
1141+ env : & Env ,
1142+ market_id : u64 ,
1143+ from_tier : u32 ,
1144+ to_tier : u32 ,
1145+ cumulative_volume : i128 ,
1146+ ) {
1147+ env. events ( ) . publish (
1148+ ( symbol_short ! ( "vol" ) , symbol_short ! ( "tier_x" ) ) ,
1149+ ( market_id, from_tier, to_tier, cumulative_volume) ,
1150+ ) ;
1151+ }
1152+
10831153/// Emit an event recording exactly how a swap's collected fee was split
10841154/// between the protocol treasury and liquidity providers. Published on every
10851155/// swap that reaches the fee-collection step, including zero-fee swaps (in
0 commit comments