@@ -6,6 +6,7 @@ use cosmwasm_std::{
66 entry_point, to_json_binary, Binary , Deps , DepsMut , Env , MessageInfo , Order ,
77 Response , StdError , StdResult , Uint128 , BankMsg , Coin ,
88} ;
9+ use std:: collections:: BTreeSet ;
910use crate :: msg:: {
1011 Auth , BatchMsg , ExecuteMsg , InstantiateMsg , MigrateMsg ,
1112 PropertyMetadata , QueryMsg , SelfCheckResponse , TreasuryAction ,
@@ -16,6 +17,28 @@ use crate::state::{
1617} ;
1718use crate :: security:: { ensure_authorized, prevent_replay} ;
1819
20+ enum RoleMutation {
21+ Grant ( String ) ,
22+ Revoke ( String ) ,
23+ }
24+
25+ fn validate_principal ( label : & str , principal : & str ) -> StdResult < String > {
26+ let normalized = principal. trim ( ) ;
27+ if normalized. is_empty ( ) {
28+ return Err ( StdError :: generic_err ( format ! ( "{} cannot be empty" , label) ) ) ;
29+ }
30+
31+ Ok ( normalized. to_string ( ) )
32+ }
33+
34+ fn format_audit_list ( entries : & [ String ] ) -> String {
35+ if entries. is_empty ( ) {
36+ "none" . to_string ( )
37+ } else {
38+ entries. join ( "," )
39+ }
40+ }
41+
1942// ── Entry points ─────────────────────────────────────────────────────────────
2043
2144#[ entry_point]
@@ -271,26 +294,95 @@ pub fn execute_update_config(
271294 new_admin : Option < String > ,
272295 authorized_roles : Option < Vec < ( String , bool ) > > ,
273296) -> StdResult < Response > {
274- let admin = ADMIN . load ( deps. storage ) ?;
275- if info. sender . as_str ( ) != admin {
297+ let current_admin = ADMIN . load ( deps. storage ) ?;
298+ if info. sender . as_str ( ) != current_admin {
276299 return Err ( StdError :: generic_err ( "Only the current admin can update config" ) ) ;
277300 }
278- if let Some ( addr) = new_admin {
279- ADMIN . save ( deps. storage , & addr) ?;
280- }
301+
302+ let next_admin = new_admin
303+ . as_deref ( )
304+ . map ( |addr| validate_principal ( "new_admin" , addr) )
305+ . transpose ( ) ?;
306+
307+ let mut seen_roles = BTreeSet :: new ( ) ;
308+ let mut role_mutations: Vec < RoleMutation > = Vec :: new ( ) ;
309+
281310 if let Some ( roles) = authorized_roles {
282311 for ( addr, is_auth) in roles {
283- AUTHORIZED_ROLES . save ( deps. storage , & addr, & is_auth) ?;
312+ let normalized = validate_principal ( "authorized role address" , & addr) ?;
313+ if !seen_roles. insert ( normalized. clone ( ) ) {
314+ return Err ( StdError :: generic_err ( format ! (
315+ "Duplicate role update requested for {}" ,
316+ normalized
317+ ) ) ) ;
318+ }
319+
320+ let existing_state = AUTHORIZED_ROLES . may_load ( deps. storage , & normalized) ?;
321+ match ( existing_state, is_auth) {
322+ ( Some ( true ) , true ) => { }
323+ ( Some ( false ) , true ) | ( None , true ) => {
324+ role_mutations. push ( RoleMutation :: Grant ( normalized) ) ;
325+ }
326+ ( Some ( true ) , false ) | ( Some ( false ) , false ) => {
327+ role_mutations. push ( RoleMutation :: Revoke ( normalized) ) ;
328+ }
329+ ( None , false ) => {
330+ return Err ( StdError :: generic_err ( format ! (
331+ "Cannot revoke role for {} because no active permission exists" ,
332+ normalized
333+ ) ) ) ;
334+ }
335+ }
336+ }
337+ }
338+
339+ let mut response = Response :: new ( )
340+ . add_attribute ( "action" , "update_config" )
341+ . add_attribute ( "actor" , info. sender . as_str ( ) ) ;
342+
343+ if let Some ( addr) = next_admin {
344+ let admin_changed = addr != current_admin;
345+ if admin_changed {
346+ ADMIN . save ( deps. storage , & addr) ?;
284347 }
348+ response = response
349+ . add_attribute ( "admin_changed" , admin_changed. to_string ( ) )
350+ . add_attribute ( "previous_admin" , current_admin. clone ( ) )
351+ . add_attribute ( "current_admin" , addr) ;
352+ } else {
353+ response = response
354+ . add_attribute ( "admin_changed" , "false" )
355+ . add_attribute ( "current_admin" , current_admin. clone ( ) ) ;
285356 }
286- Ok ( Response :: new ( ) . add_attribute ( "action" , "update_config" ) )
357+
358+ let mut granted_roles: Vec < String > = Vec :: new ( ) ;
359+ let mut revoked_roles: Vec < String > = Vec :: new ( ) ;
360+
361+ for mutation in role_mutations {
362+ match mutation {
363+ RoleMutation :: Grant ( addr) => {
364+ AUTHORIZED_ROLES . save ( deps. storage , & addr, & true ) ?;
365+ granted_roles. push ( addr) ;
366+ }
367+ RoleMutation :: Revoke ( addr) => {
368+ AUTHORIZED_ROLES . remove ( deps. storage , & addr) ;
369+ revoked_roles. push ( addr) ;
370+ }
371+ }
372+ }
373+
374+ Ok ( response
375+ . add_attribute ( "roles_granted_count" , granted_roles. len ( ) . to_string ( ) )
376+ . add_attribute ( "roles_revoked_count" , revoked_roles. len ( ) . to_string ( ) )
377+ . add_attribute ( "roles_granted" , format_audit_list ( & granted_roles) )
378+ . add_attribute ( "roles_revoked" , format_audit_list ( & revoked_roles) ) )
287379}
288380
289381/// #136 — Storage Cleanup Utility
290382///
291383/// Removes:
292384/// - Zero-balance entries from `FEE_BALANCES` (saves storage rent)
293- /// - Explicitly revoked role entries (value == false) from `AUTHORIZED_ROLES`
385+ /// - Legacy explicitly- revoked role entries (value == false) from `AUTHORIZED_ROLES`
294386///
295387/// Admin-only. Does not affect treasury, metadata, or nonce state.
296388pub fn execute_cleanup_storage (
@@ -316,7 +408,7 @@ pub fn execute_cleanup_storage(
316408 FEE_BALANCES . remove ( deps. storage , key. as_str ( ) ) ;
317409 }
318410
319- // Collect revoked role keys (explicitly set to false — dead entries)
411+ // Collect legacy revoked role keys (explicitly set to false — dead entries)
320412 let revoked_role_keys: Vec < String > = AUTHORIZED_ROLES
321413 . range ( deps. storage , None , None , Order :: Ascending )
322414 . filter_map ( |item| {
@@ -400,7 +492,7 @@ pub fn query_self_check(deps: Deps) -> StdResult<SelfCheckResponse> {
400492 ) ) ;
401493 }
402494
403- // 5. No explicitly-revoked role entries should remain (they waste storage)
495+ // 5. No legacy explicitly-revoked role entries should remain (they waste storage)
404496 let revoked_role_count = AUTHORIZED_ROLES
405497 . range ( deps. storage , None , None , Order :: Ascending )
406498 . filter_map ( |item| item. ok ( ) )
@@ -409,7 +501,7 @@ pub fn query_self_check(deps: Deps) -> StdResult<SelfCheckResponse> {
409501
410502 if revoked_role_count > 0 {
411503 failures. push ( format ! (
412- "{} authorized_roles entries are explicitly false (consider running CleanupStorage)" ,
504+ "{} authorized_roles entries use the legacy false tombstone format (consider running CleanupStorage)" ,
413505 revoked_role_count
414506 ) ) ;
415507 } else {
0 commit comments