Skip to content

Commit 923ccd7

Browse files
authored
Merge pull request #931 from joshfatoye0011-bit/fix/716-expiry-comparison-and-member-count
fix(router-access): align expiry semantics and fix RoleMemberCount bookkeeping
2 parents 24d28e9 + 5aff485 commit 923ccd7

1 file changed

Lines changed: 56 additions & 10 deletions

File tree

  • contracts/router-access/src

contracts/router-access/src/lib.rs

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,12 @@ impl RouterAccess {
191191
}
192192

193193
/// Check if a role has expired for an address.
194+
///
195+
/// Returns `true` only when the current ledger timestamp **strictly exceeds**
196+
/// `expires_at`, matching the convention used throughout this suite:
197+
/// `expires_at` is the **last valid** timestamp, so the role is still active
198+
/// when `current_timestamp == expires_at` and expired only once
199+
/// `current_timestamp > expires_at`.
194200
pub fn is_role_expired(env: Env, role: String, target: Address) -> bool {
195201
// View helper: counter is maintained for active members, but expiry still
196202
// uses RoleExpiry storage.
@@ -201,7 +207,7 @@ impl RouterAccess {
201207
.get::<DataKey, u64>(&DataKey::RoleExpiry(role, target))
202208
{
203209
let current_timestamp = env.ledger().timestamp();
204-
current_timestamp >= expires_at
210+
current_timestamp > expires_at
205211
} else {
206212
false
207213
}
@@ -615,12 +621,6 @@ impl RouterAccess {
615621
) -> Result<(), AccessError> {
616622
caller.require_auth();
617623
router_common::require_admin_simple!(&env, &caller, &DataKey::SuperAdmin, AccessError)?;
618-
env.storage()
619-
.instance()
620-
.remove(&DataKey::RoleExpiry(role.clone(), target.clone()));
621-
env.storage()
622-
.instance()
623-
.remove(&DataKey::HasRole(role.clone(), target.clone()));
624624
Self::require_super_admin(&env, &caller)?;
625625
Self::deactivate_role_grant(&env, &role, &target);
626626
env.events().publish(
@@ -809,7 +809,8 @@ impl RouterAccess {
809809
// the requested expiry matches the existing expiry.
810810
//
811811
// This allows admins to extend/shorten expiry (or remove it by granting with `None`).
812-
if has_raw_assignment && Self::has_role_internal(env, account, role) {
812+
let currently_active = has_raw_assignment && Self::has_role_internal(env, account, role);
813+
if currently_active {
813814
let existing_expiry: Option<u64> = env
814815
.storage()
815816
.instance()
@@ -846,6 +847,22 @@ impl RouterAccess {
846847
.instance()
847848
.set(&DataKey::HasRole(role.clone(), account.clone()), &true);
848849

850+
// Increment RoleMemberCount when the account transitions from inactive to active.
851+
// This covers two cases:
852+
// 1. Brand-new grant (no prior assignment).
853+
// 2. Re-grant of a previously expired role (raw assignment exists but was inactive).
854+
// An expiry update on a live role must NOT increment to avoid double-counting.
855+
if !currently_active {
856+
let count: u32 = env
857+
.storage()
858+
.instance()
859+
.get::<DataKey, u32>(&DataKey::RoleMemberCount(role.clone()))
860+
.unwrap_or(0);
861+
env.storage()
862+
.instance()
863+
.set(&DataKey::RoleMemberCount(role.clone()), &(count + 1));
864+
}
865+
849866
let mut members: Vec<Address> = env
850867
.storage()
851868
.instance()
@@ -971,14 +988,17 @@ impl RouterAccess {
971988
return false;
972989
}
973990

974-
// Check if role has expired
991+
// Check if role has expired.
992+
// `expires_at` is the last valid timestamp: the role is still active when
993+
// `current_timestamp == expires_at` and only expired once it strictly
994+
// exceeds `expires_at`, consistent with `is_route_expired` in router-core.
975995
if let Some(expires_at) = env
976996
.storage()
977997
.instance()
978998
.get::<DataKey, u64>(&DataKey::RoleExpiry(role.clone(), account.clone()))
979999
{
9801000
let current_timestamp = env.ledger().timestamp();
981-
if current_timestamp >= expires_at {
1001+
if current_timestamp > expires_at {
9821002
return false;
9831003
}
9841004
}
@@ -1036,6 +1056,32 @@ mod tests {
10361056
assert!(!client.has_role(&user, &role));
10371057
}
10381058

1059+
/// `expires_at` is the **last valid** timestamp: the role must still be
1060+
/// active when `current_timestamp == expires_at` and only expired once
1061+
/// `current_timestamp > expires_at`. This mirrors the semantics of
1062+
/// `is_route_expired` in router-core and makes the boundary consistent
1063+
/// across the entire suite.
1064+
#[test]
1065+
fn test_role_valid_at_exact_expiry_timestamp_expired_one_second_after() {
1066+
let (env, admin, client) = setup();
1067+
let role = String::from_str(&env, "operator");
1068+
let user = Address::generate(&env);
1069+
1070+
let now = env.ledger().timestamp();
1071+
client.grant_role(&admin, &user, &role, &Some(10));
1072+
// expires_at == now + 10
1073+
1074+
// At exactly expires_at the role is still valid.
1075+
env.ledger().set_timestamp(now + 10);
1076+
assert!(client.has_role(&user, &role));
1077+
assert!(!client.is_role_expired(&role, &user));
1078+
1079+
// One second past expires_at the role is expired.
1080+
env.ledger().set_timestamp(now + 11);
1081+
assert!(!client.has_role(&user, &role));
1082+
assert!(client.is_role_expired(&role, &user));
1083+
}
1084+
10391085
#[test]
10401086
fn test_set_role_admin_emits_event() {
10411087
let (env, admin, client) = setup();

0 commit comments

Comments
 (0)