68903 sc low users cannot revoke a migration permit after the migrator loses migrator role allowing stale approval to reactivate if the same address is re granted the role
Submitted on Mar 11th 2026 at 21:30:32 UTC by @iam0x04 for Audit Comp | Folks Finance: Staking Contracts
Report ID: #68903
Report Type: Smart Contract
Report severity: Low
Target: https://github.com/Folks-Finance/folks-staking-contracts/blob/main/src/Staking.sol
Impacts:
a stale approval can later be reused to migrate a user’s active position without fresh consent.
Description
Brief/Intro
setMigrationPermit() blocks both granting and revoking permission behind a live MIGRATOR_ROLE check. As a result, if a user previously approved a migrator, and that migrator later loses its role, the user can no longer clear the stored approval. If the same address is granted MIGRATOR_ROLE again in the future, the stale approval becomes active again and the migrator can move the user’s still-open staking position without fresh consent, breaking the intended “user-controlled migration” model.
Vulnerability Details
function setMigrationPermit(address _migrator, bool _isMigrationPermitted) external {
if (!hasRole(MIGRATOR_ROLE, _migrator)) revert MigratorNotFound(_migrator);
migrationPermits[_migrator][msg.sender] = _isMigrationPermitted;
emit MigrationPermitUpdated(_migrator, msg.sender, _isMigrationPermitted);
}https://github.com/Folks-Finance/folks-staking-contracts/blob/3131a2d46b5afa76f606bf08adfd85452a47e2d8/src/Staking.sol#L77-L82
This function applies the same hasRole(MIGRATOR_ROLE, _migrator) gate to both:
enabling a permit with
truedisabling a permit with
false
That creates the following broken state transition:
A user grants migration permission to migrator M by calling
setMigrationPermit(M, true).Later, M loses
MIGRATOR_ROLE.The user attempts to revoke the old approval with
setMigrationPermit(M, false).The call reverts with
MigratorNotFound(M)because M no longer has the role.The old value in
migrationPermits[M][user]remainstrue.If M is re-granted
MIGRATOR_ROLElater, the stale approval is immediately valid again.
The stale approval is then consumed by migratePositionsFrom()
Once the role is re-granted, the old permit is enough for the migrator to pass authorization and receive the user’s remaining principal and rewards from the staking contract.
This is also inconsistent with the documentation in README.md (line 136), which states that migration permission "can be revoked at any time."
https://github.com/Folks-Finance/folks-staking-contracts/blob/3131a2d46b5afa76f606bf08adfd85452a47e2d8/README.md?plain=1#L134-L140
Impact Details
The impact is that a stale migration approval can later be reused to migrate a user’s active position without fresh consent if the same address is re-granted MIGRATOR_ROLE.
Proof of Concept
Put this file under the test folder.
Alice stakes
10 etherinto the staking contract.Alice explicitly grants migration permission to
migrator.Admin revokes
MIGRATOR_ROLEfrommigrator.Alice tries to revoke the old permission, but the call reverts with
MigratorNotFound.The stored approval remains
trueeven though the migrator was deauthorized.Admin grants
MIGRATOR_ROLEback to the samemigratoraddress.The migrator successfully calls
migratePositionsFrom(alice)using the stale approval.The test confirms Alice’s stake is removed and the migrator receives Alice’s principal plus reward.
Was this helpful?