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:
A user approves a migrator once.
The protocol removes
MIGRATOR_ROLEfrom that migrator.The user can no longer call
setMigrationPermit(migrator, false)because the function now reverts withMigratorNotFound.At any later time, governance can grant the same address
MIGRATOR_ROLEagain.The old
migrationPermits[migrator][user] == truemapping 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
Alice stakes into V1.
Alice approves
migratorviasetMigrationPermit(migrator, true).Admin revokes
MIGRATOR_ROLEfrommigrator.Alice attempts to revoke approval, but
setMigrationPermit(migrator, false)reverts.Admin grants
MIGRATOR_ROLEback to the same address.migratorcallsmigratePositionsFrom(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.solTest:
test_PoC_RepeatedMigrationConsumesCapWhileV1RemainsActive
Run:
folks-staking-contracts-main/test/StakingImmunefiPoC.t.sol:
Was this helpful?