69476 sc low users cannot revoke stale migration approvals after a migrator is offboarded so old permits can silently reactivate

Submitted on Mar 15th 2026 at 05:20:09 UTC by @Johnyfwesh for Audit Comp | Folks Finance: Staking Contracts

  • Report ID: #69476

  • Report Type: Smart Contract

  • Report severity: Low

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

  • Impacts:

    • Users cannot revoke

Finding Description and Impact

Staking.setMigrationPermit() uses the migrator's current role membership as a hard precondition for every write. That includes revocations: calling setMigrationPermit(migrator, false) reverts with MigratorNotFound unless the target address currently holds MIGRATOR_ROLE. However, the user's approval is persisted separately in migrationPermits and role removal does not clear that storage.

This creates a permission-lifecycle bug during migrator rotation or incident response. A user can approve a migrator while it is active, but once the address is offboarded the same user loses the ability to revoke that old approval. The stale true entry remains stored indefinitely. If operations later re-grant MIGRATOR_ROLE to the same address, migratePositionsFrom() accepts the old permit and immediately re-enables migration without fresh user consent.

That reactivation is not just cosmetic. Once re-authorized, the migrator can pull the user's remaining principal and rewards directly to itself via TOKEN.safeTransfer(msg.sender, ...). This undermines the intended approval model described in IMigratorV1, where migration is supposed to require current user permission.

Affected code

StakingPeriod[] public stakingPeriods;
mapping(address user => UserStake[]) public userStakes;
mapping(address migrator => mapping(address user => bool isAuthorized)) public migrationPermits;
  • Migration approvals are stored independently from role lifecycle. Revoking MIGRATOR_ROLE does not clear any existing user consent entries.

  • Grants and revocations both require the target address to currently hold MIGRATOR_ROLE, so users cannot clear an old permit once the migrator is offboarded.

  • Once the same address regains MIGRATOR_ROLE, the old stored migrationPermits[msg.sender][user] == true entry is immediately sufficient again.

  • Successful migration sends the user's remaining assets directly to the migrator, so stale-approval reactivation can lead to real unauthorized asset movement.

  • The interface documents a consent-based security model, but stale permits can outlive role removal and revive later without renewed approval.

Impact

Users cannot revoke migration consent during migrator offboarding. That makes role removal weaker than it appears operationally: the system may look safe while the address is deauthorized, yet an old true permit remains in storage waiting to reactivate if the same address is regranted. it can immediately migrate any previously approved user's active positions without asking again. Because migratePositionsFrom() transfers unclaimed principal and rewards to the migrator address, this can result in unauthorized custody transfer of user assets.

  1. Allow setMigrationPermit(_migrator, false) to succeed even when _migrator no longer has MIGRATOR_ROLE; only require the role when setting a permit to true.

  2. Invalidate or clear stored permits when MIGRATOR_ROLE is revoked, for example by deleting permits or advancing a per-migrator approval epoch that must match at migration time.

  3. Add regression tests that cover the full lifecycle: approve, revoke role, user revokes, regrant role, and assert migration stays blocked until the user explicitly approves again.

Proof of Concept

PoC Test

Test file

test/StaleMigrationPermitReactivation.t.sol

What the PoC demonstrates

  1. Alice stakes and explicitly approves an active migrator.

  2. Admin revokes MIGRATOR_ROLE from that migrator address.

  3. Alice tries to clean up with setMigrationPermit(migrator, false) and the call reverts with MigratorNotFound.

  4. The old migrationPermits[migrator][alice] value remains true in storage.

  5. Admin grants MIGRATOR_ROLE back to the same address.

  6. The same migrator immediately calls migratePositionsFrom(alice) and receives Alice's remaining stake plus reward without any fresh authorization step.

Run

Test Results

The passing PoC confirms the issue end to end: Alice cannot revoke the stale approval once the migrator is offboarded, the stored permit survives role removal, and regranting the same address reactivates migration immediately without new user consent.

Was this helpful?