69100 sc low permit irrevocability after migrator role revocation
Submitted on Mar 12th 2026 at 20:25:12 UTC by @Petrate for Audit Comp | Folks Finance: Staking Contracts
Report ID: #69100
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)
Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
Description
Permit Irrevocable When Migrator Role Is Revoked
The user cannot protect themselves (cannot revoke consent) even though the documentation guarantees they can. No direct theft occurs at this step alone, but the user suffers loss of control over their funds with no profit motive required from the attacker. The re-grant attack path escalates this to Direct theft of any user funds if MIGRATOR_ROLE is re-granted.
Contract: Staking.sol → setMigrationPermit()
setMigrationPermit() in Staking.sol applies a hasRole(MIGRATOR_ROLE, _migrator) check to both the grant (true) and revoke (false) paths. Once a migrator's role is revoked, users can no longer call setMigrationPermit(migrator, false) — it always reverts with MigratorNotFound. The permit is permanently stuck as true with no way for the user to clear it.
The documentation makes a direct, verifiable false promise:
"The permission can be revoked at any time by calling
setMigrationPermit(migratorAddress, false)"
Root Cause
The hasRole check is unconditional — it applies regardless of whether the caller is granting or revoking. Revoking should never require the migrator to still hold the role, since the user is only modifying their own consent record.
Impact
A user who granted a migration permit to Migrator M cannot revoke it once M's MIGRATOR_ROLE is revoked. If the admin later re-grants MIGRATOR_ROLE to M (e.g., after resolving a compromised key incident), M's stale permit immediately reactivates and M can call migratePositionsFrom(user) to drain the user's stakes — without any new consent from the user.
Attack Path
Migrator M holds MIGRATOR_ROLE; user grants permit via setMigrationPermit(M, true)
M's role is revoked (compromised key, admin action)
User calls setMigrationPermit(M, false) to protect themselves → REVERTS with MigratorNotFound
Admin re-grants MIGRATOR_ROLE to M after resolving the incident
M's permit for the user is live again — no new user action required
M calls migratePositionsFrom(user) and drains all user stakes
Known Issue Distinction
The published known issue states:
"State
migrationPermitsmay contain migrator which had itsMIGRATOR_ROLElater revoked"
This acknowledges the stale state as an observation. It does not acknowledge that users lose the ability to clean it up — which is what the documentation explicitly guarantees they can do.
Recommended Fix
Link to Proof of Concept
https://gist.github.com/devpetrate/02b3234b4403ea993ebc66f7dad7f766
Proof of Concept
Migrator M holds MIGRATOR_ROLE; user calls setMigrationPermit(M, true) to grant permission
Admin calls revokeRole(MIGRATOR_ROLE, M) — M's role is removed
User calls setMigrationPermit(M, false) to protect themselves
Transaction reverts with MigratorNotFound — user cannot revoke
Admin calls grantRole(MIGRATOR_ROLE, M) to restore the role
migrationPermits[M][user] is still true — no new user action needed
M calls migratePositionsFrom(user) and drains all user stakes without any new consent from the user
PoC Output (verified locally)
Doc Contradiction
The README explicitly states:
"The permission can be revoked at any time by calling
setMigrationPermit(migratorAddress, false)"
This is demonstrably false once the migrator's role has been revoked.
Known Issue Distinction
The published known issue states:
"State migrationPermits may contain migrator which had its MIGRATOR_ROLE later revoked"
This acknowledges the stale state only as an observation. It does not acknowledge that users lose the ability to clean it up — which is what the documentation explicitly guarantees they can do.
Full Runnable PoC
Full source code, contracts, and test files available at: https://gist.github.com/devpetrate/02b3234b4403ea993ebc66f7dad7f766
Run:
Was this helpful?