@@ -211,6 +211,12 @@ pub enum DataKey {
211211 /// seconds have elapsed since `PairLastRouteAt`. Capped at
212212 /// `MAX_COOLDOWN_SECS` (30 days). Defaults to `0` (disabled).
213213 PairCooldown ( Symbol , Symbol ) ,
214+ /// `true` when `(source, destination)` has an active dispute flag
215+ /// (keyed per-pair, `bool`, persistent). Set by `flag_pair_dispute`
216+ /// and cleared by `resolve_pair_dispute` or `unregister_pair`. While
217+ /// `true`, `quote_route` and `compute_route_fee` reject the pair with
218+ /// `PairDisputed`. Defaults to `false`.
219+ PairDisputed ( Symbol , Symbol ) ,
214220 /// Optional absolute per-route fee ceiling (singleton, `i128`,
215221 /// persistent). When set, the effective fee is `min(bps_fee, cap)`.
216222 /// Absent ↔ `None` (only the relative `MAX_FEE_BPS` bound applies).
@@ -320,6 +326,9 @@ pub enum RouterError {
320326 /// route free. Use [`StableRouteRouter::clear_max_fee_absolute`] to
321327 /// remove the cap entirely.
322328 ZeroFeeCap = 21 ,
329+ /// `quote_route` or `compute_route_fee` was called for a pair with an
330+ /// active dispute flag (see [`StableRouteRouter::flag_pair_dispute`]).
331+ PairDisputed = 22 ,
323332}
324333
325334/// StableRoute router contract — placeholder for routing logic.
@@ -608,6 +617,46 @@ impl StableRouteRouter {
608617 ) ;
609618 }
610619
620+ /// Admin flags `(source, destination)` as disputed. While disputed,
621+ /// [`Self::quote_route`] and [`Self::compute_route_fee`] reject the
622+ /// pair with [`RouterError::PairDisputed`] until
623+ /// [`Self::resolve_pair_dispute`] clears the flag. Requires the pair
624+ /// to already be registered.
625+ ///
626+ /// Idempotent on the event: flagging an already-disputed pair
627+ /// re-asserts the flag but does not re-emit `disp_set`.
628+ pub fn flag_pair_dispute ( env : Env , source : Symbol , destination : Symbol ) {
629+ Self :: require_admin ( & env) ;
630+ Self :: require_pair_registered ( & env, & source, & destination) ;
631+ let already_disputed = Self :: read_pair_disputed ( & env, & source, & destination) ;
632+ env. storage ( ) . persistent ( ) . set (
633+ & DataKey :: PairDisputed ( source. clone ( ) , destination. clone ( ) ) ,
634+ & true ,
635+ ) ;
636+ if !already_disputed {
637+ env. events ( )
638+ . publish ( ( symbol_short ! ( "disp_set" ) , ) , ( source, destination, true ) ) ;
639+ }
640+ }
641+
642+ /// Admin clears a dispute flag, restoring normal routing and quoting
643+ /// for the pair.
644+ ///
645+ /// Idempotent: resolving a pair with no active dispute is a clean
646+ /// no-op that does not emit `disp_set`.
647+ pub fn resolve_pair_dispute ( env : Env , source : Symbol , destination : Symbol ) {
648+ Self :: require_admin ( & env) ;
649+ let was_disputed = Self :: read_pair_disputed ( & env, & source, & destination) ;
650+ env. storage ( ) . persistent ( ) . set (
651+ & DataKey :: PairDisputed ( source. clone ( ) , destination. clone ( ) ) ,
652+ & false ,
653+ ) ;
654+ if was_disputed {
655+ env. events ( )
656+ . publish ( ( symbol_short ! ( "disp_set" ) , ) , ( source, destination, false ) ) ;
657+ }
658+ }
659+
611660 /// Configure an absolute upper bound on the fee charged for any route.
612661 ///
613662 /// Admin-gated. The computed fee is clamped to this value after the
@@ -1089,6 +1138,14 @@ impl StableRouteRouter {
10891138 Self :: read_pair_registered ( & env, & source, & destination)
10901139 }
10911140
1141+ /// Returns `true` if `(source, destination)` currently has an active
1142+ /// dispute flag set via [`Self::flag_pair_dispute`].
1143+ ///
1144+ /// Does not mutate storage. Defaults to `false`.
1145+ pub fn is_pair_disputed ( env : Env , source : Symbol , destination : Symbol ) -> bool {
1146+ Self :: read_pair_disputed ( & env, & source, & destination)
1147+ }
1148+
10921149 /// Returns the configured routing fee for the pair, expressed in basis
10931150 /// points.
10941151 ///
@@ -1117,6 +1174,9 @@ impl StableRouteRouter {
11171174 panic_with_error ! ( & env, RouterError :: AmountMustBePositive ) ;
11181175 }
11191176 Self :: require_pair_registered ( & env, & source, & destination) ;
1177+ if Self :: read_pair_disputed ( & env, & source, & destination) {
1178+ panic_with_error ! ( & env, RouterError :: PairDisputed ) ;
1179+ }
11201180 if matches ! (
11211181 env. storage( )
11221182 . persistent( )
@@ -1203,6 +1263,9 @@ impl StableRouteRouter {
12031263 if !Self :: read_pair_registered ( & env, & source, & destination) {
12041264 Self :: route_abort ( & env, RouterError :: PairNotRegistered ) ;
12051265 }
1266+ if Self :: read_pair_disputed ( & env, & source, & destination) {
1267+ Self :: route_abort ( & env, RouterError :: PairDisputed ) ;
1268+ }
12061269 let min_amount = Self :: read_pair_min ( & env, & source, & destination) ;
12071270 if amount < min_amount {
12081271 Self :: route_abort ( & env, RouterError :: AmountBelowMin ) ;
@@ -1563,6 +1626,18 @@ impl StableRouteRouter {
15631626 . unwrap_or ( false )
15641627 }
15651628
1629+ /// Read whether `(source, destination)` currently has an active
1630+ /// dispute flag. Single source of truth for the
1631+ /// [`DataKey::PairDisputed`] sentinel value; shared by `quote_route`
1632+ /// and `compute_route_fee` instead of each inlining its own storage
1633+ /// read.
1634+ fn read_pair_disputed ( env : & Env , source : & Symbol , destination : & Symbol ) -> bool {
1635+ env. storage ( )
1636+ . persistent ( )
1637+ . get ( & DataKey :: PairDisputed ( source. clone ( ) , destination. clone ( ) ) )
1638+ . unwrap_or ( false )
1639+ }
1640+
15661641 /// Read the per-pair fee in basis points from persistent storage.
15671642 ///
15681643 /// Returns `0` (free) when the slot is absent — the documented
@@ -1672,7 +1747,8 @@ impl StableRouteRouter {
16721747 storage. remove ( & DataKey :: PairMinAmount ( source. clone ( ) , destination. clone ( ) ) ) ;
16731748 storage. remove ( & DataKey :: PairMaxAmount ( source. clone ( ) , destination. clone ( ) ) ) ;
16741749 storage. remove ( & DataKey :: PairLiquidity ( source. clone ( ) , destination. clone ( ) ) ) ;
1675- storage. remove ( & DataKey :: PairCooldown ( source, destination) ) ;
1750+ storage. remove ( & DataKey :: PairCooldown ( source. clone ( ) , destination. clone ( ) ) ) ;
1751+ storage. remove ( & DataKey :: PairDisputed ( source, destination) ) ;
16761752 }
16771753
16781754 /// Remove a registered pair from the router.
@@ -2315,6 +2391,102 @@ mod test {
23152391 client. register_pair ( & symbol_short ! ( "USDC" ) , & symbol_short ! ( "USDC" ) ) ;
23162392 }
23172393
2394+ // --- dispute flag ---
2395+
2396+ #[ test]
2397+ fn test_is_pair_disputed_defaults_to_false ( ) {
2398+ let env = Env :: default ( ) ;
2399+ let ( client, _admin) = setup_initialized ( & env) ;
2400+ let ( s, d) = ( symbol_short ! ( "USDC" ) , symbol_short ! ( "EURC" ) ) ;
2401+ client. register_pair ( & s, & d) ;
2402+ assert ! ( !client. is_pair_disputed( & s, & d) ) ;
2403+ }
2404+
2405+ #[ test]
2406+ fn test_flag_pair_dispute_blocks_quote_and_compute ( ) {
2407+ let env = Env :: default ( ) ;
2408+ let ( client, _admin) = setup_initialized ( & env) ;
2409+ let ( s, d) = ( symbol_short ! ( "USDC" ) , symbol_short ! ( "EURC" ) ) ;
2410+ client. register_pair ( & s, & d) ;
2411+ client. flag_pair_dispute ( & s, & d) ;
2412+ assert ! ( client. is_pair_disputed( & s, & d) ) ;
2413+
2414+ let quote_result = client. try_quote_route ( & s, & d, & 1_000i128 ) ;
2415+ assert ! ( quote_result. is_err( ) , "disputed pair must reject quote_route" ) ;
2416+
2417+ let compute_result = client. try_compute_route_fee ( & s, & d, & 1_000i128 ) ;
2418+ assert ! (
2419+ compute_result. is_err( ) ,
2420+ "disputed pair must reject compute_route_fee"
2421+ ) ;
2422+ }
2423+
2424+ #[ test]
2425+ fn test_resolve_pair_dispute_restores_routing ( ) {
2426+ let env = Env :: default ( ) ;
2427+ let ( client, _admin) = setup_initialized ( & env) ;
2428+ let ( s, d) = ( symbol_short ! ( "USDC" ) , symbol_short ! ( "EURC" ) ) ;
2429+ client. register_pair ( & s, & d) ;
2430+ client. flag_pair_dispute ( & s, & d) ;
2431+ client. resolve_pair_dispute ( & s, & d) ;
2432+ assert ! ( !client. is_pair_disputed( & s, & d) ) ;
2433+ // Routing works again once resolved.
2434+ client. quote_route ( & s, & d, & 1_000i128 ) ;
2435+ }
2436+
2437+ #[ test]
2438+ #[ should_panic( expected = "Error(Contract, #5)" ) ]
2439+ fn test_flag_pair_dispute_requires_registered_pair ( ) {
2440+ let env = Env :: default ( ) ;
2441+ let ( client, _admin) = setup_initialized ( & env) ;
2442+ client. flag_pair_dispute ( & symbol_short ! ( "USDC" ) , & symbol_short ! ( "EURC" ) ) ;
2443+ }
2444+
2445+ #[ test]
2446+ fn test_flag_pair_dispute_does_not_duplicate_event_on_reflag ( ) {
2447+ let env = Env :: default ( ) ;
2448+ let ( client, _admin) = setup_initialized ( & env) ;
2449+ let ( s, d) = ( symbol_short ! ( "USDC" ) , symbol_short ! ( "EURC" ) ) ;
2450+ client. register_pair ( & s, & d) ;
2451+ client. flag_pair_dispute ( & s, & d) ;
2452+ client. flag_pair_dispute ( & s, & d) ;
2453+ assert_eq ! (
2454+ event_payloads( & env, symbol_short!( "disp_set" ) ) . len( ) ,
2455+ 1 ,
2456+ "re-flagging an already-disputed pair must not emit a second disp_set event"
2457+ ) ;
2458+ }
2459+
2460+ #[ test]
2461+ fn test_resolve_pair_dispute_on_undisputed_pair_is_noop_without_event ( ) {
2462+ let env = Env :: default ( ) ;
2463+ let ( client, _admin) = setup_initialized ( & env) ;
2464+ let ( s, d) = ( symbol_short ! ( "USDC" ) , symbol_short ! ( "EURC" ) ) ;
2465+ client. register_pair ( & s, & d) ;
2466+ client. resolve_pair_dispute ( & s, & d) ;
2467+ assert ! ( !client. is_pair_disputed( & s, & d) ) ;
2468+ assert_eq ! (
2469+ event_payloads( & env, symbol_short!( "disp_set" ) ) . len( ) ,
2470+ 0 ,
2471+ "resolving a pair with no active dispute must not emit disp_set"
2472+ ) ;
2473+ }
2474+
2475+ #[ test]
2476+ fn test_unregister_pair_clears_dispute_flag ( ) {
2477+ let env = Env :: default ( ) ;
2478+ let ( client, _admin) = setup_initialized ( & env) ;
2479+ let ( s, d) = ( symbol_short ! ( "USDC" ) , symbol_short ! ( "EURC" ) ) ;
2480+ client. register_pair ( & s, & d) ;
2481+ client. flag_pair_dispute ( & s, & d) ;
2482+ client. unregister_pair ( & s, & d) ;
2483+ client. register_pair ( & s, & d) ;
2484+ assert ! (
2485+ !client. is_pair_disputed( & s, & d) ,
2486+ "re-registering after unregister must not inherit a stale dispute flag"
2487+ ) ;
2488+ }
2489+
23182490 #[ test]
23192491 fn test_is_pair_registered_defaults_to_false ( ) {
23202492 let env = Env :: default ( ) ;
0 commit comments