For the complete documentation index, see llms.txt. This page is also available as Markdown.

69663 sc low users cannot revoke previously granted migration permit after migrator role is revoked

Submitted on Mar 16th 2026 at 06:52:27 UTC by @Oxb4b for Audit Comp | Folks Finance: Staking Contracts

  • Report ID: #69663

  • Report Type: Smart Contract

  • Report severity: Low

  • Target: https://github.com/Folks-Finance/folks-staking-contracts/blob/main/src/Staking.sol

  • Impacts:

    • Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)

Description

Brief/Intro

The setMigrationPermit() function in Staking.sol prevents users from revoking previously granted migration permissions once the migrator's role has been revoked by the admin. This creates a consent-revocation deadlock where stale permissions persist indefinitely and can become active again if the same address is later re-granted the MIGRATOR_ROLE, allowing migrations without fresh user consent.

Vulnerability Details

Current Implementation

function setMigrationPermit(address _migrator, bool _isMigrationPermitted) external {
    if (!hasRole(MIGRATOR_ROLE, _migrator)) revert MigratorNotFound(_migrator);
    
    migrationPermits[_migrator][msg.sender] = _isMigrationPermitted;
    emit MigrationPermitUpdated(_migrator, msg.sender, _isMigrationPermitted);
}

The function enforces hasRole(MIGRATOR_ROLE, _migrator) check regardless of whether the user is granting or revoking permission. This design has an unintended consequence:

  • Granting permission (true): The role check is appropriate — users should only grant permits to active migrators.

  • Revoking permission (false): The role check creates a deadlock — users cannot revoke previously granted permissions if the migrator no longer holds the role.

Root Cause

The migrationPermits mapping is never cleared when a migrator loses the MIGRATOR_ROLE. The single gate (hasRole check) applies to both grant and revoke operations, preventing users from cleaning up stale consent after role revocation.

  1. Alice grants migration permission to migrator M: setMigrationPermit(M, true)migrationPermits[M][Alice] = true

  2. Admin revokes M's role: revokeRole(MIGRATOR_ROLE, M)

  3. Alice attempts to revoke her old permit: setMigrationPermit(M, false)

  4. Call reverts with MigratorNotFound(M) because hasRole(MIGRATOR_ROLE, M) now returns false

  5. Months later, admin re-grants role: grantRole(MIGRATOR_ROLE, M)

  6. M can now migrate Alice's positions using the stale permit from step 1, without any fresh consent from Alice


Impact and Likelihood Details

Impact: Low

  • No Direct Fund Loss: Funds are not stolen; migration is a legitimate protocol operation.

  • Loss of User Autonomy: Users lose the ability to revoke previously granted migration consent, violating the principle that consent should always be user-controlled.

  • Unexpected Migration: Users who attempted to revoke consent (and were blocked) may be surprised when old permissions become active again after role re-grant.

Likelihood: Low

  • Requires specific operational sequence: user grants permit → role revoked → user attempts revocation → role re-granted to same address.

  • However, the bug is deterministic once the sequence occurs — every user who granted a permit before role revocation will experience this issue.

https://gist.github.com/tharunbethina/36b3cf2705b65633fc2fd05fe6d8e599

Proof of Concept

1

Alice grants permission to migrator M

setMigrationPermit(M, true)migrationPermits[M][Alice] = true

2

Admin revokes MIGRATOR_ROLE from M

revokeRole(MIGRATOR_ROLE, M)

3

Alice tries to revoke old permit

setMigrationPermit(M, false)

4

Call reverts

Call reverts with MigratorNotFound(M) (role check blocks revocation)

5

Stale permit persists in storage

migrationPermits[M][Alice] remains true

6

Admin later re-grants MIGRATOR_ROLE to M

grantRole(MIGRATOR_ROLE, M)

7

M migrates Alice's positions

M migrates Alice's positions via migratePositionsFrom(Alice) despite Alice's explicit revocation attempt - her consent intent (false) was ignored due to the deadlock

Was this helpful?