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

69926 sc low users cannot revoke migration permits after migrator role is removed enabling fund migration without re consent

Submitted on Mar 17th 2026 at 12:30:50 UTC by @dmitriia for Audit Comp | Folks Finance: Staking Contracts

  • Report ID: #69926

  • Report Type: Smart Contract

  • Report severity: Low

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

  • Impacts:

    • Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield

Description

Brief/Intro

setMigrationPermit() enforces hasRole(MIGRATOR_ROLE, _migrator) for both granting and revoking permits. Once the admin revokes MIGRATOR_ROLE from an address, users who previously granted that address a permit can no longer call setMigrationPermit(_migrator, false) because the role check reverts. The stale permit persists in storage. If MIGRATOR_ROLE is later re-granted to the same address, all stale permits become active again and migration proceeds without the user's re-consent.

Vulnerability Details

The migration mechanism requires two independent authorizations:

  1. Admin grants MIGRATOR_ROLE to a contract address.

  2. User calls setMigrationPermit(migrator, true) to authorize that address.

When the admin revokes MIGRATOR_ROLE, both authorizations should become independently manageable. Instead, the hasRole guard in setMigrationPermit() blocks all calls — including revocations — creating a one-way lock on the user's permit.

Attack path:

  1. Admin grants MIGRATOR_ROLE to address M for a legitimate V1 → V2 migration window.

  2. Alice calls setMigrationPermit(M, true) during the window.

  3. Migration window closes; admin revokes MIGRATOR_ROLE from M.

  4. Alice attempts setMigrationPermit(M, false) → reverts with MigratorNotFound(M).

  5. Time passes. Admin re-grants MIGRATOR_ROLE to M for a new migration cycle.

  6. M calls migratePositionsFrom(alice) — succeeds because the stale permit is still true.

  7. Alice's unclaimed principal and rewards are transferred to M without Alice ever re-consenting.

Root cause is that the hasRole check does not distinguish between granting (true) and revoking (false). I.e. revoking a permit is a user-protective action that is advised to be available unconditionally and it's now not.

As a mitigation consider controlling the role check with the _isMigrationPermitted flag:

Staking.sol#L77-L82

Impact Details

Likelihood — Low. Requires the admin to revoke and later re-grant MIGRATOR_ROLE to the same address. This is plausible during multi-phase migration rollouts, address reuse, or misconfiguration.

Impact — High. When the path completes, all unclaimed principal and accrued rewards for every user with a stale permit are transferred to the migrator address without any user interaction or additional consent, i.e. the user has no on-chain mechanism to prevent this.

Severity — Low. Low likelihood combined with direct loss of user funds.

References

setMigrationPermit() unconditionally checks hasRole(), blocking permit revocation after role removal:

Staking.sol#L77-L82

migratePositionsFrom() reads the stale permit and proceeds to transfer all unclaimed funds:

Staking.sol#L166-L210

The permit mapping is never cleared by migratePositionsFrom() or any other function, so it survives across role grant/revoke cycles:

Staking.sol#L38

Proof of Concept

Was this helpful?