@@ -205,6 +205,12 @@ pub enum DataKey {
205205 /// seconds have elapsed since `PairLastRouteAt`. Capped at
206206 /// `MAX_COOLDOWN_SECS` (30 days). Defaults to `0` (disabled).
207207 PairCooldown ( Symbol , Symbol ) ,
208+ /// `true` when `(source, destination)` has an active dispute flag
209+ /// (keyed per-pair, `bool`, persistent). Set by `flag_pair_dispute`
210+ /// and cleared by `resolve_pair_dispute` or `unregister_pair`. While
211+ /// `true`, `quote_route` and `compute_route_fee` reject the pair with
212+ /// `PairDisputed`. Defaults to `false`.
213+ PairDisputed ( Symbol , Symbol ) ,
208214 /// Optional absolute per-route fee ceiling (singleton, `i128`,
209215 /// persistent). When set, the effective fee is `min(bps_fee, cap)`.
210216 /// Absent ↔ `None` (only the relative `MAX_FEE_BPS` bound applies).
@@ -314,6 +320,9 @@ pub enum RouterError {
314320 /// route free. Use [`StableRouteRouter::clear_max_fee_absolute`] to
315321 /// remove the cap entirely.
316322 ZeroFeeCap = 21 ,
323+ /// `quote_route` or `compute_route_fee` was called for a pair with an
324+ /// active dispute flag (see [`StableRouteRouter::flag_pair_dispute`]).
325+ PairDisputed = 22 ,
317326}
318327
319328/// StableRoute router contract — placeholder for routing logic.
@@ -624,6 +633,46 @@ impl StableRouteRouter {
624633 ) ;
625634 }
626635
636+ /// Admin flags `(source, destination)` as disputed. While disputed,
637+ /// [`Self::quote_route`] and [`Self::compute_route_fee`] reject the
638+ /// pair with [`RouterError::PairDisputed`] until
639+ /// [`Self::resolve_pair_dispute`] clears the flag. Requires the pair
640+ /// to already be registered.
641+ ///
642+ /// Idempotent on the event: flagging an already-disputed pair
643+ /// re-asserts the flag but does not re-emit `disp_set`.
644+ pub fn flag_pair_dispute ( env : Env , source : Symbol , destination : Symbol ) {
645+ Self :: require_admin ( & env) ;
646+ Self :: require_pair_registered ( & env, & source, & destination) ;
647+ let already_disputed = Self :: read_pair_disputed ( & env, & source, & destination) ;
648+ env. storage ( ) . persistent ( ) . set (
649+ & DataKey :: PairDisputed ( source. clone ( ) , destination. clone ( ) ) ,
650+ & true ,
651+ ) ;
652+ if !already_disputed {
653+ env. events ( )
654+ . publish ( ( symbol_short ! ( "disp_set" ) , ) , ( source, destination, true ) ) ;
655+ }
656+ }
657+
658+ /// Admin clears a dispute flag, restoring normal routing and quoting
659+ /// for the pair.
660+ ///
661+ /// Idempotent: resolving a pair with no active dispute is a clean
662+ /// no-op that does not emit `disp_set`.
663+ pub fn resolve_pair_dispute ( env : Env , source : Symbol , destination : Symbol ) {
664+ Self :: require_admin ( & env) ;
665+ let was_disputed = Self :: read_pair_disputed ( & env, & source, & destination) ;
666+ env. storage ( ) . persistent ( ) . set (
667+ & DataKey :: PairDisputed ( source. clone ( ) , destination. clone ( ) ) ,
668+ & false ,
669+ ) ;
670+ if was_disputed {
671+ env. events ( )
672+ . publish ( ( symbol_short ! ( "disp_set" ) , ) , ( source, destination, false ) ) ;
673+ }
674+ }
675+
627676 /// Configure an absolute upper bound on the fee charged for any route.
628677 ///
629678 /// Admin-gated. The computed fee is clamped to this value after the
@@ -1097,6 +1146,14 @@ impl StableRouteRouter {
10971146 Self :: read_pair_registered ( & env, & source, & destination)
10981147 }
10991148
1149+ /// Returns `true` if `(source, destination)` currently has an active
1150+ /// dispute flag set via [`Self::flag_pair_dispute`].
1151+ ///
1152+ /// Does not mutate storage. Defaults to `false`.
1153+ pub fn is_pair_disputed ( env : Env , source : Symbol , destination : Symbol ) -> bool {
1154+ Self :: read_pair_disputed ( & env, & source, & destination)
1155+ }
1156+
11001157 /// Returns the configured routing fee for the pair, expressed in basis
11011158 /// points.
11021159 ///
@@ -1125,6 +1182,9 @@ impl StableRouteRouter {
11251182 panic_with_error ! ( & env, RouterError :: AmountMustBePositive ) ;
11261183 }
11271184 Self :: require_pair_registered ( & env, & source, & destination) ;
1185+ if Self :: read_pair_disputed ( & env, & source, & destination) {
1186+ panic_with_error ! ( & env, RouterError :: PairDisputed ) ;
1187+ }
11281188 if matches ! (
11291189 env. storage( )
11301190 . persistent( )
@@ -1211,6 +1271,9 @@ impl StableRouteRouter {
12111271 if !Self :: read_pair_registered ( & env, & source, & destination) {
12121272 Self :: route_abort ( & env, RouterError :: PairNotRegistered ) ;
12131273 }
1274+ if Self :: read_pair_disputed ( & env, & source, & destination) {
1275+ Self :: route_abort ( & env, RouterError :: PairDisputed ) ;
1276+ }
12141277 let min_amount = Self :: read_pair_min ( & env, & source, & destination) ;
12151278 if amount < min_amount {
12161279 Self :: route_abort ( & env, RouterError :: AmountBelowMin ) ;
@@ -1510,6 +1573,18 @@ impl StableRouteRouter {
15101573 . unwrap_or ( false )
15111574 }
15121575
1576+ /// Read whether `(source, destination)` currently has an active
1577+ /// dispute flag. Single source of truth for the
1578+ /// [`DataKey::PairDisputed`] sentinel value; shared by `quote_route`
1579+ /// and `compute_route_fee` instead of each inlining its own storage
1580+ /// read.
1581+ fn read_pair_disputed ( env : & Env , source : & Symbol , destination : & Symbol ) -> bool {
1582+ env. storage ( )
1583+ . persistent ( )
1584+ . get ( & DataKey :: PairDisputed ( source. clone ( ) , destination. clone ( ) ) )
1585+ . unwrap_or ( false )
1586+ }
1587+
15131588 /// Read the per-pair fee in basis points from persistent storage.
15141589 ///
15151590 /// Returns `0` (free) when the slot is absent — the documented
@@ -1619,7 +1694,8 @@ impl StableRouteRouter {
16191694 storage. remove ( & DataKey :: PairMinAmount ( source. clone ( ) , destination. clone ( ) ) ) ;
16201695 storage. remove ( & DataKey :: PairMaxAmount ( source. clone ( ) , destination. clone ( ) ) ) ;
16211696 storage. remove ( & DataKey :: PairLiquidity ( source. clone ( ) , destination. clone ( ) ) ) ;
1622- storage. remove ( & DataKey :: PairCooldown ( source, destination) ) ;
1697+ storage. remove ( & DataKey :: PairCooldown ( source. clone ( ) , destination. clone ( ) ) ) ;
1698+ storage. remove ( & DataKey :: PairDisputed ( source, destination) ) ;
16231699 }
16241700
16251701 /// Remove a registered pair from the router.
@@ -2262,6 +2338,102 @@ mod test {
22622338 client. register_pair ( & symbol_short ! ( "USDC" ) , & symbol_short ! ( "USDC" ) ) ;
22632339 }
22642340
2341+ // --- dispute flag ---
2342+
2343+ #[ test]
2344+ fn test_is_pair_disputed_defaults_to_false ( ) {
2345+ let env = Env :: default ( ) ;
2346+ let ( client, _admin) = setup_initialized ( & env) ;
2347+ let ( s, d) = ( symbol_short ! ( "USDC" ) , symbol_short ! ( "EURC" ) ) ;
2348+ client. register_pair ( & s, & d) ;
2349+ assert ! ( !client. is_pair_disputed( & s, & d) ) ;
2350+ }
2351+
2352+ #[ test]
2353+ fn test_flag_pair_dispute_blocks_quote_and_compute ( ) {
2354+ let env = Env :: default ( ) ;
2355+ let ( client, _admin) = setup_initialized ( & env) ;
2356+ let ( s, d) = ( symbol_short ! ( "USDC" ) , symbol_short ! ( "EURC" ) ) ;
2357+ client. register_pair ( & s, & d) ;
2358+ client. flag_pair_dispute ( & s, & d) ;
2359+ assert ! ( client. is_pair_disputed( & s, & d) ) ;
2360+
2361+ let quote_result = client. try_quote_route ( & s, & d, & 1_000i128 ) ;
2362+ assert ! ( quote_result. is_err( ) , "disputed pair must reject quote_route" ) ;
2363+
2364+ let compute_result = client. try_compute_route_fee ( & s, & d, & 1_000i128 ) ;
2365+ assert ! (
2366+ compute_result. is_err( ) ,
2367+ "disputed pair must reject compute_route_fee"
2368+ ) ;
2369+ }
2370+
2371+ #[ test]
2372+ fn test_resolve_pair_dispute_restores_routing ( ) {
2373+ let env = Env :: default ( ) ;
2374+ let ( client, _admin) = setup_initialized ( & env) ;
2375+ let ( s, d) = ( symbol_short ! ( "USDC" ) , symbol_short ! ( "EURC" ) ) ;
2376+ client. register_pair ( & s, & d) ;
2377+ client. flag_pair_dispute ( & s, & d) ;
2378+ client. resolve_pair_dispute ( & s, & d) ;
2379+ assert ! ( !client. is_pair_disputed( & s, & d) ) ;
2380+ // Routing works again once resolved.
2381+ client. quote_route ( & s, & d, & 1_000i128 ) ;
2382+ }
2383+
2384+ #[ test]
2385+ #[ should_panic( expected = "Error(Contract, #5)" ) ]
2386+ fn test_flag_pair_dispute_requires_registered_pair ( ) {
2387+ let env = Env :: default ( ) ;
2388+ let ( client, _admin) = setup_initialized ( & env) ;
2389+ client. flag_pair_dispute ( & symbol_short ! ( "USDC" ) , & symbol_short ! ( "EURC" ) ) ;
2390+ }
2391+
2392+ #[ test]
2393+ fn test_flag_pair_dispute_does_not_duplicate_event_on_reflag ( ) {
2394+ let env = Env :: default ( ) ;
2395+ let ( client, _admin) = setup_initialized ( & env) ;
2396+ let ( s, d) = ( symbol_short ! ( "USDC" ) , symbol_short ! ( "EURC" ) ) ;
2397+ client. register_pair ( & s, & d) ;
2398+ client. flag_pair_dispute ( & s, & d) ;
2399+ client. flag_pair_dispute ( & s, & d) ;
2400+ assert_eq ! (
2401+ event_payloads( & env, symbol_short!( "disp_set" ) ) . len( ) ,
2402+ 1 ,
2403+ "re-flagging an already-disputed pair must not emit a second disp_set event"
2404+ ) ;
2405+ }
2406+
2407+ #[ test]
2408+ fn test_resolve_pair_dispute_on_undisputed_pair_is_noop_without_event ( ) {
2409+ let env = Env :: default ( ) ;
2410+ let ( client, _admin) = setup_initialized ( & env) ;
2411+ let ( s, d) = ( symbol_short ! ( "USDC" ) , symbol_short ! ( "EURC" ) ) ;
2412+ client. register_pair ( & s, & d) ;
2413+ client. resolve_pair_dispute ( & s, & d) ;
2414+ assert ! ( !client. is_pair_disputed( & s, & d) ) ;
2415+ assert_eq ! (
2416+ event_payloads( & env, symbol_short!( "disp_set" ) ) . len( ) ,
2417+ 0 ,
2418+ "resolving a pair with no active dispute must not emit disp_set"
2419+ ) ;
2420+ }
2421+
2422+ #[ test]
2423+ fn test_unregister_pair_clears_dispute_flag ( ) {
2424+ let env = Env :: default ( ) ;
2425+ let ( client, _admin) = setup_initialized ( & env) ;
2426+ let ( s, d) = ( symbol_short ! ( "USDC" ) , symbol_short ! ( "EURC" ) ) ;
2427+ client. register_pair ( & s, & d) ;
2428+ client. flag_pair_dispute ( & s, & d) ;
2429+ client. unregister_pair ( & s, & d) ;
2430+ client. register_pair ( & s, & d) ;
2431+ assert ! (
2432+ !client. is_pair_disputed( & s, & d) ,
2433+ "re-registering after unregister must not inherit a stale dispute flag"
2434+ ) ;
2435+ }
2436+
22652437 #[ test]
22662438 fn test_is_pair_registered_defaults_to_false ( ) {
22672439 let env = Env :: default ( ) ;
0 commit comments