69908 sc low stale migration approvals cannot be revoked after role revocation and automatically reactivate on role re grant

Submitted on Mar 17th 2026 at 11:34:47 UTC by @Dec3mber for Audit Comp | Folks Finance: Staking Contracts

  • Report ID: #69908

  • 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

setMigrationPermit() requires the target address to currently hold MIGRATOR_ROLE even when a user is trying to revoke an old approval. If governance/admin revokes the role first, the user permanently loses the ability to clear the stale approval entry. If the same address later regains MIGRATOR_ROLE, the historical approval becomes active again without fresh user consent.

Root cause

The contract uses the same role check for both granting and revoking migration approval:

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

This makes revocation dependent on the protocol keeping the target address authorized, which defeats the purpose of a user-controlled opt-out.

Vulnerability Details

The intended trust model is that migration requires the user's active approval. In practice, that approval becomes sticky:

  1. A user approves a migrator once.

  2. The protocol removes MIGRATOR_ROLE from that migrator.

  3. The user can no longer call setMigrationPermit(migrator, false) because the function now reverts with MigratorNotFound.

  4. At any later time, governance can grant the same address MIGRATOR_ROLE again.

  5. The old migrationPermits[migrator][user] == true mapping entry immediately becomes valid again.

At that point, the migrator can move the user's positions again without any renewed consent.

Impact Details

This breaks the advertised "user-controlled migration" security property. A stale approval can survive an entire role lifecycle and silently reactivate in the future. The result is a griefing vector against users: positions can be migrated after the user reasonably believes the prior authorization is no longer live.

Attack path

  1. Alice stakes into V1.

  2. Alice approves migrator via setMigrationPermit(migrator, true).

  3. Admin revokes MIGRATOR_ROLE from migrator.

  4. Alice attempts to revoke approval, but setMigrationPermit(migrator, false) reverts.

  5. Admin grants MIGRATOR_ROLE back to the same address.

  6. migrator calls migratePositionsFrom(alice) and succeeds without any fresh approval from Alice.

References

https://github.com/Folks-Finance/folks-staking-contracts/blob/3131a2d46b5afa76f606bf08adfd85452a47e2d8/src/Staking.sol#L77-L82 https://github.com/Folks-Finance/folks-staking-contracts/blob/3131a2d46b5afa76f606bf08adfd85452a47e2d8/src/Staking.sol#L163-L210

Proof of Concept

Runnable Foundry PoC:

  • File: folks-staking-contracts-main/test/StakingImmunefiPoC.t.sol

  • Test: test_PoC_RepeatedMigrationConsumesCapWhileV1RemainsActive

Run:

folks-staking-contracts-main/test/StakingImmunefiPoC.t.sol:

Was this helpful?