69463 sc low stale migration permits can be reactivated by re granting migrator role to a previously approved migrator

Submitted on Mar 15th 2026 at 02:24:39 UTC by @Nescient for Audit Comp | Folks Finance: Staking Contracts

  • Report ID: #69463

  • 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

The README describes migration as "User-controlled migration" and states that "No migration can happen without the user's active approval" and that "The permission can be revoked at any time by calling setMigrationPermit(migratorAddress, false)." However, this is not true in all cases: if a user approved a migrator and that migrator later loses MIGRATOR_ROLE, the old permit remains stored but the user can no longer revoke it; if the same address is granted MIGRATOR_ROLE again later, the stale approval becomes active again without fresh user consent, exposing the user's open stakes to unintended migration.

Vulnerability Details

This issue is different from the two acknowledged behaviors. It is not about a permit persisting after migration, and it is not merely about migrationPermits containing an address whose MIGRATOR_ROLE was revoked. The actual vulnerability is that once the role is revoked, the user loses the ability to clear that stale permit, so the old approval can later be reactivated simply by re-granting MIGRATOR_ROLE to the same address. In other words, the known issue is stale state existence; this report shows that the stale state is user-unrevocable and can become valid authorization again.

The issue is that setMigrationPermit() blocks both opt-in and opt-out behind the same role check:

if (!hasRole(MIGRATOR_ROLE, _migrator)) revert MigratorNotFound(_migrator);
migrationPermits[_migrator][msg.sender] = _isMigrationPermitted;

At the same time, migration only requires the migrator to currently have MIGRATOR_ROLE and for the stored permit to still be true:

This creates the following stale-permission flow:

1

User approves migrator M

User approves migrator M.

2

Admin revokes MIGRATOR_ROLE from M

Admin revokes MIGRATOR_ROLE from M.

3

User tries to revoke approval

User tries to revoke approval, but setMigrationPermit(M, false) reverts because M no longer has the role.

4

Old permit remains stored

The old value in migrationPermits[M][user] remains true.

5

Role is re-granted

If MIGRATOR_ROLE is later re-granted to M, the old approval becomes usable again.

So the problem is not just that permits persist after role revocation, but that users are prevented from clearing them during that period. This also contradicts the README statements that migration is user-controlled and that permission can be revoked at any time.

Impact Details

This can re-activate old migration approvals without a new user opt-in, exposing all still-open stakes of affected users to unintended migration. In the provided example migrator, migrate(user) is permissionless, so once the role is restored any caller can trigger migration for users who still have a stale approval. The issue is best classified as a Low severity stale-authorization / broken-revocation vulnerability.

References

setMigrationPermit: https://github.com/Folks-Finance/folks-staking-contracts/blob/3131a2d46b5afa76f606bf08adfd85452a47e2d8/src/Staking.sol#L77-L81 migratePositionsFrom : https://github.com/Folks-Finance/folks-staking-contracts/blob/3131a2d46b5afa76f606bf08adfd85452a47e2d8/src/Staking.sol#L166-L172

Proof of Concept

Runnable proof of concept for the stale migration permit issue.

Flow:

1

Alice grants migration permission

Alice grants migration permission to a migrator while it has MIGRATOR_ROLE.

2

Admin revokes the role

Admin revokes MIGRATOR_ROLE from that migrator.

3

Alice can no longer revoke

Alice can no longer call setMigrationPermit(migrator, false), so the old permit stays stored.

4

Admin grants the role back

Admin grants MIGRATOR_ROLE back to the same migrator address.

5

Migration can be triggered again

Any caller can trigger migration through the example MigratorV1 using Alice's stale approval.

You can copy the PoC below and run it using the following command:

The test passes successfully and the following is the console output:

Was this helpful?