@@ -122,65 +122,7 @@ impl RouterAccess {
122122 return Err ( AccessError :: RoleNotFound ) ;
123123 }
124124
125- // Decrement active-member counter only if this grant was currently active.
126- // (If the role is expired, it may still exist in HasRole but shouldn't be
127- // counted as an active member.)
128- let was_active = Self :: has_role_internal ( & env, & target, & role) ;
129-
130- env. storage ( ) . instance ( ) . remove ( & key) ;
131-
132- if was_active {
133- let current: u32 = env
134- . storage ( )
135- . instance ( )
136- . get :: < DataKey , u32 > ( & DataKey :: RoleMemberCount ( role. clone ( ) ) )
137- . unwrap_or ( 0u32 ) ;
138- let new_count = current. saturating_sub ( 1 ) ;
139- env. storage ( )
140- . instance ( )
141- . set ( & DataKey :: RoleMemberCount ( role. clone ( ) ) , & new_count) ;
142- }
143-
144- let mut members: Vec < Address > = env
145- . storage ( )
146- . instance ( )
147- . get ( & DataKey :: RoleMembers ( role. clone ( ) ) )
148- . unwrap_or_else ( || Vec :: new ( & env) ) ;
149- if let Some ( i) = members. iter ( ) . position ( |a| a == target) {
150- members. remove ( i as u32 ) ;
151- }
152- env. storage ( )
153- . instance ( )
154- . set ( & DataKey :: RoleMembers ( role. clone ( ) ) , & members) ;
155-
156- let mut roles: Vec < String > = env
157- . storage ( )
158- . instance ( )
159- . get ( & DataKey :: AddressRoles ( target. clone ( ) ) )
160- . unwrap_or_else ( || Vec :: new ( & env) ) ;
161- if let Some ( i) = roles. iter ( ) . position ( |r| r == role) {
162- roles. remove ( i as u32 ) ;
163- }
164- env. storage ( )
165- . instance ( )
166- . set ( & DataKey :: AddressRoles ( target. clone ( ) ) , & roles) ;
167-
168- env. storage ( )
169- . instance ( )
170- . remove ( & DataKey :: RoleExpiry ( role. clone ( ) , target. clone ( ) ) ) ;
171-
172- // Keep RoleMemberCount consistent for expiry-based removal.
173- if Self :: has_role_internal ( & env, & target, & role) {
174- let current: u32 = env
175- . storage ( )
176- . instance ( )
177- . get :: < DataKey , u32 > ( & DataKey :: RoleMemberCount ( role. clone ( ) ) )
178- . unwrap_or ( 0u32 ) ;
179- let new_count = current. saturating_sub ( 1 ) ;
180- env. storage ( )
181- . instance ( )
182- . set ( & DataKey :: RoleMemberCount ( role. clone ( ) ) , & new_count) ;
183- }
125+ Self :: deactivate_role_grant ( & env, & role, & target) ;
184126
185127 env. events ( ) . publish (
186128 ( Symbol :: new ( & env, router_common:: EVENT_ROLE_REVOKED ) , ) ,
@@ -241,7 +183,7 @@ impl RouterAccess {
241183 admin : Address ,
242184 ) -> Result < ( ) , AccessError > {
243185 caller. require_auth ( ) ;
244- Self :: require_super_admin ( & env, & caller) ?;
186+ router_common :: require_admin_simple! ( & env, & caller, & DataKey :: SuperAdmin , AccessError ) ?;
245187 if Self :: is_blacklisted_internal ( & env, & admin) {
246188 return Err ( AccessError :: Blacklisted ) ;
247189 }
@@ -272,7 +214,7 @@ impl RouterAccess {
272214 parent_role : String ,
273215 ) -> Result < ( ) , AccessError > {
274216 caller. require_auth ( ) ;
275- Self :: require_super_admin ( & env, & caller) ?;
217+ router_common :: require_admin_simple! ( & env, & caller, & DataKey :: SuperAdmin , AccessError ) ?;
276218 Self :: ensure_no_role_parent_cycle ( & env, & role, & parent_role) ?;
277219
278220 Self :: track_role_in_all_roles ( & env, & role) ;
@@ -318,7 +260,7 @@ impl RouterAccess {
318260 /// Blacklist an address.
319261 pub fn blacklist ( env : Env , caller : Address , target : Address ) -> Result < ( ) , AccessError > {
320262 caller. require_auth ( ) ;
321- Self :: require_super_admin ( & env, & caller) ?;
263+ router_common :: require_admin_simple! ( & env, & caller, & DataKey :: SuperAdmin , AccessError ) ?;
322264
323265 let super_admin: Address = env
324266 . storage ( )
@@ -349,7 +291,7 @@ impl RouterAccess {
349291 /// Remove from blacklist.
350292 pub fn unblacklist ( env : Env , caller : Address , target : Address ) -> Result < ( ) , AccessError > {
351293 caller. require_auth ( ) ;
352- Self :: require_super_admin ( & env, & caller) ?;
294+ router_common :: require_admin_simple! ( & env, & caller, & DataKey :: SuperAdmin , AccessError ) ?;
353295 env. storage ( )
354296 . instance ( )
355297 . remove ( & DataKey :: Blacklisted ( target. clone ( ) ) ) ;
@@ -589,7 +531,11 @@ impl RouterAccess {
589531 new_admin : Address ,
590532 ) -> Result < ( ) , AccessError > {
591533 current. require_auth ( ) ;
534+ router_common:: require_admin_simple!( & env, & current, & DataKey :: SuperAdmin , AccessError ) ?;
592535 Self :: require_super_admin ( & env, & current) ?;
536+ if Self :: is_blacklisted_internal ( & env, & new_admin) {
537+ return Err ( AccessError :: Blacklisted ) ;
538+ }
593539 env. storage ( )
594540 . instance ( )
595541 . set ( & DataKey :: SuperAdmin , & new_admin) ;
@@ -614,13 +560,15 @@ impl RouterAccess {
614560 target : Address ,
615561 ) -> Result < ( ) , AccessError > {
616562 caller. require_auth ( ) ;
617- Self :: require_super_admin ( & env, & caller) ?;
563+ router_common :: require_admin_simple! ( & env, & caller, & DataKey :: SuperAdmin , AccessError ) ?;
618564 env. storage ( )
619565 . instance ( )
620566 . remove ( & DataKey :: RoleExpiry ( role. clone ( ) , target. clone ( ) ) ) ;
621567 env. storage ( )
622568 . instance ( )
623569 . remove ( & DataKey :: HasRole ( role. clone ( ) , target. clone ( ) ) ) ;
570+ Self :: require_super_admin ( & env, & caller) ?;
571+ Self :: deactivate_role_grant ( & env, & role, & target) ;
624572 env. events ( ) . publish (
625573 ( Symbol :: new ( & env, router_common:: EVENT_ROLE_EXPIRED ) , ) ,
626574 ( role, target) ,
@@ -630,6 +578,59 @@ impl RouterAccess {
630578
631579 // ── Helpers ───────────────────────────────────────────────────────────────
632580
581+ /// Fully deactivates a role grant: removes `HasRole`, decrements
582+ /// `RoleMemberCount` if the grant was currently active, and removes the
583+ /// grant from `RoleMembers(role)`, `AddressRoles(target)`, and
584+ /// `RoleExpiry(role, target)`. Shared by `revoke_role` and `expire_role`
585+ /// so both leave identical, consistent bookkeeping behind.
586+ fn deactivate_role_grant ( env : & Env , role : & String , target : & Address ) {
587+ let was_active = Self :: has_role_internal ( env, target, role) ;
588+
589+ env. storage ( )
590+ . instance ( )
591+ . remove ( & DataKey :: HasRole ( role. clone ( ) , target. clone ( ) ) ) ;
592+
593+ if was_active {
594+ let current: u32 = env
595+ . storage ( )
596+ . instance ( )
597+ . get :: < DataKey , u32 > ( & DataKey :: RoleMemberCount ( role. clone ( ) ) )
598+ . unwrap_or ( 0u32 ) ;
599+ let new_count = current. saturating_sub ( 1 ) ;
600+ env. storage ( )
601+ . instance ( )
602+ . set ( & DataKey :: RoleMemberCount ( role. clone ( ) ) , & new_count) ;
603+ }
604+
605+ let mut members: Vec < Address > = env
606+ . storage ( )
607+ . instance ( )
608+ . get ( & DataKey :: RoleMembers ( role. clone ( ) ) )
609+ . unwrap_or_else ( || Vec :: new ( env) ) ;
610+ if let Some ( i) = members. iter ( ) . position ( |a| a == * target) {
611+ members. remove ( i as u32 ) ;
612+ }
613+ env. storage ( )
614+ . instance ( )
615+ . set ( & DataKey :: RoleMembers ( role. clone ( ) ) , & members) ;
616+
617+ let mut roles: Vec < String > = env
618+ . storage ( )
619+ . instance ( )
620+ . get ( & DataKey :: AddressRoles ( target. clone ( ) ) )
621+ . unwrap_or_else ( || Vec :: new ( env) ) ;
622+ if let Some ( i) = roles. iter ( ) . position ( |r| r == * role) {
623+ roles. remove ( i as u32 ) ;
624+ }
625+ env. storage ( )
626+ . instance ( )
627+ . set ( & DataKey :: AddressRoles ( target. clone ( ) ) , & roles) ;
628+
629+ env. storage ( )
630+ . instance ( )
631+ . remove ( & DataKey :: RoleExpiry ( role. clone ( ) , target. clone ( ) ) ) ;
632+ }
633+
633634 /// Track a role name in the AllRoles list if it hasn't been seen before.
634635 fn track_role_in_all_roles ( env : & Env , role : & String ) {
635636 let mut all_roles: Vec < String > = env
@@ -651,7 +652,27 @@ impl RouterAccess {
651652 AccessError :: InvalidExpiry => router_common:: BatchItemError :: Custom (
652653 soroban_sdk:: String :: from_str ( env, "InvalidExpiry" ) ,
653654 ) ,
654- _ => router_common:: BatchItemError :: Custom ( soroban_sdk:: String :: from_str ( env, "Error" ) ) ,
655+ AccessError :: AlreadyInitialized => router_common:: BatchItemError :: Custom (
656+ soroban_sdk:: String :: from_str ( env, "AlreadyInitialized" ) ,
657+ ) ,
658+ AccessError :: NotInitialized => router_common:: BatchItemError :: Custom (
659+ soroban_sdk:: String :: from_str ( env, "NotInitialized" ) ,
660+ ) ,
661+ AccessError :: RoleNotFound => router_common:: BatchItemError :: Custom (
662+ soroban_sdk:: String :: from_str ( env, "RoleNotFound" ) ,
663+ ) ,
664+ AccessError :: CannotBlacklistAdmin => router_common:: BatchItemError :: Custom (
665+ soroban_sdk:: String :: from_str ( env, "CannotBlacklistAdmin" ) ,
666+ ) ,
667+ AccessError :: DestinationAlreadyHasRole => router_common:: BatchItemError :: Custom (
668+ soroban_sdk:: String :: from_str ( env, "DestinationAlreadyHasRole" ) ,
669+ ) ,
670+ AccessError :: HierarchyCycle => router_common:: BatchItemError :: Custom (
671+ soroban_sdk:: String :: from_str ( env, "HierarchyCycle" ) ,
672+ ) ,
673+ AccessError :: HierarchyTooDeep => router_common:: BatchItemError :: Custom (
674+ soroban_sdk:: String :: from_str ( env, "HierarchyTooDeep" ) ,
675+ ) ,
655676 }
656677 }
657678
@@ -783,7 +804,11 @@ impl RouterAccess {
783804 Ok ( ( ) )
784805 }
785806
807+
786808 fn require_super_admin ( env : & Env , caller : & Address ) -> Result < ( ) , AccessError > {
809+ if Self :: is_blacklisted_internal ( env, caller) {
810+ return Err ( AccessError :: Blacklisted ) ;
811+ }
787812 let admin: Address = env
788813 . storage ( )
789814 . instance ( )
@@ -1593,6 +1618,73 @@ mod tests {
15931618 assert_eq ! ( client. get_role_parent( & viewer) , Some ( editor) ) ;
15941619 }
15951620
1621+ // ── Issue #819: require_super_admin / transfer_super_admin blacklist gap ──
1622+
1623+ #[ test]
1624+ fn test_blacklisted_address_cannot_regain_super_admin_via_transfer ( ) {
1625+ let ( env, admin_a, client) = setup ( ) ;
1626+ let admin_b = Address :: generate ( & env) ;
1627+
1628+ // (1) A transfers super-admin to B.
1629+ client. transfer_super_admin ( & admin_a, & admin_b) ;
1630+ assert_eq ! ( client. super_admin( ) , admin_b) ;
1631+
1632+ // (2) B blacklists A — allowed, since A is no longer the current
1633+ // super-admin.
1634+ client. blacklist ( & admin_b, & admin_a) ;
1635+ assert ! ( client. is_blacklisted( & admin_a) ) ;
1636+
1637+ // (3) B attempts to transfer super-admin back to the now-blacklisted
1638+ // A. This must be rejected.
1639+ let result = client. try_transfer_super_admin ( & admin_b, & admin_a) ;
1640+ assert_eq ! ( result, Err ( Ok ( AccessError :: Blacklisted ) ) ) ;
1641+
1642+ // Super-admin remains B; A never regains privileged authority.
1643+ assert_eq ! ( client. super_admin( ) , admin_b) ;
1644+
1645+ // Even if A were to somehow be reinstated, a blacklisted caller must
1646+ // be rejected up front by require_super_admin. Simulate this by
1647+ // checking a blacklisted address can never pass the super-admin gate
1648+ // for any privileged action, using set_role_admin as a probe.
1649+ let role = String :: from_str ( & env, "operator" ) ;
1650+ let victim = Address :: generate ( & env) ;
1651+ let probe = client. try_set_role_admin ( & admin_a, & role, & victim) ;
1652+ assert_eq ! ( probe, Err ( Ok ( AccessError :: Unauthorized ) ) ) ;
1653+ }
1654+
1655+ #[ test]
1656+ fn test_transfer_super_admin_rejects_blacklisted_new_admin_directly ( ) {
1657+ let ( env, admin, client) = setup ( ) ;
1658+ let blacklisted = Address :: generate ( & env) ;
1659+
1660+ client. blacklist ( & admin, & blacklisted) ;
1661+ assert ! ( client. is_blacklisted( & blacklisted) ) ;
1662+
1663+ let result = client. try_transfer_super_admin ( & admin, & blacklisted) ;
1664+ assert_eq ! ( result, Err ( Ok ( AccessError :: Blacklisted ) ) ) ;
1665+ assert_eq ! ( client. super_admin( ) , admin) ;
1666+ }
1667+
1668+ // ── Issue #820: expire_role bookkeeping parity with revoke_role ──────────
1669+
1670+ #[ test]
1671+ fn test_expire_role_clears_member_count_and_address_roles ( ) {
1672+ let ( env, admin, client) = setup ( ) ;
1673+ let role = String :: from_str ( & env, "operator" ) ;
1674+ let user = Address :: generate ( & env) ;
1675+
1676+ client. grant_role ( & admin, & user, & role, & Some ( 9999 ) ) ;
1677+ assert_eq ! ( client. get_role_member_count( & role) , 1 ) ;
1678+ assert ! ( client. get_roles_for_address( & user) . contains( & role) ) ;
1679+ assert ! ( client. get_role_members( & role) . contains( & user) ) ;
1680+
1681+ client. expire_role ( & admin, & role, & user) ;
1682+
1683+ assert_eq ! ( client. get_role_member_count( & role) , 0 ) ;
1684+ assert ! ( !client. get_roles_for_address( & user) . contains( & role) ) ;
1685+ assert ! ( !client. get_role_members( & role) . contains( & user) ) ;
1686+ }
1687+
15961688 #[ test]
15971689 fn test_set_role_parent_rejects_transitive_cycle ( ) {
15981690 let ( env, admin, client) = setup ( ) ;
0 commit comments