69738 sc low setmigrationpermit prevents users from revoking stale permits after migrator role is revoked

Submitted on Mar 16th 2026 at 15:35:40 UTC by @ZenHunter for Audit Comp | Folks Finance: Staking Contracts

  • Report ID: #69738

  • Report Type: Smart Contract

  • Report severity: Low

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

  • Impacts:

    • Contract fails to deliver promised returns, but doesn't lose value

Description

Brief/Intro

setMigrationPermit enforces hasRole(MIGRATOR_ROLE, _migrator) for both granting and revoking a migration permit. When a user previously approved a migrator whose role was later revoked by the admin, the user's attempt to revoke their own permit reverts with MigratorNotFound. The stale migrationPermits[_migrator][user] = true persists in storage with no cleanup path.

Vulnerability Details

setMigrationPermit applies the same role check regardless of whether the caller is granting or revoking:

// src/Staking.sol#L77-L82
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);
}

When _migrator no longer holds MIGRATOR_ROLE, the check !hasRole(MIGRATOR_ROLE, _migrator) is true and the function reverts — even when _isMigrationPermitted is false (i.e., the user is trying to revoke). No other function modifies migrationPermits, so the stale entry has no cleanup path.

The known issues list acknowledges that stale permits exist:

"State migrationPermits may contain migrator which had its MIGRATOR_ROLE later revoked"

However, it does not acknowledge the root cause: the hasRole check in setMigrationPermit actively blocks the user from revoking the stale permit themselves. The user is left with no recourse.

Impact Details

If the admin re-grants MIGRATOR_ROLE to the same address — for a V3 migration, a key rotation, or after a compromise is believed resolved — every stale permit reactivates immediately. The migrator can call migratePositionsFrom(user) for any affected user and receive all of their unclaimed principal and rewards in a single call.

Users with stakes still inside the staking period (before unlockTime) are in the worst position: they cannot withdraw their tokens to empty the position, and they cannot revoke the permit. Their funds are fully exposed for as long as the stale permit persists.

References

  • Affected function: src/Staking.sol#L77-L82 (setMigrationPermit)

  • Migration execution: src/Staking.sol#L166-L210 (migratePositionsFrom)

Recommendation

Only enforce the hasRole check when granting a permit, not when revoking:

This allows users to revoke permits at any time regardless of the migrator's current role status, while still preventing users from granting permits to addresses that do not hold the role.

Proof of Concept

Output:

Alice's setMigrationPermit(migrator, false) reverts with MigratorNotFound. The stale permit persists. When the admin re-grants the role, the migrator immediately drains Alice's full 1,100 FOLKS (1,000 principal + 100 reward at 10% APR) without any renewed consent from Alice.

Was this helpful?