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_ROLEdoes 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 storedmigrationPermits[msg.sender][user] == trueentry 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.
Recommended mitigation steps
Allow
setMigrationPermit(_migrator, false)to succeed even when_migratorno longer hasMIGRATOR_ROLE; only require the role when setting a permit totrue.Invalidate or clear stored permits when
MIGRATOR_ROLEis revoked, for example by deleting permits or advancing a per-migrator approval epoch that must match at migration time.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
Alice stakes and explicitly approves an active migrator.
Admin revokes
MIGRATOR_ROLEfrom that migrator address.Alice tries to clean up with
setMigrationPermit(migrator, false)and the call reverts withMigratorNotFound.The old
migrationPermits[migrator][alice]value remainstruein storage.Admin grants
MIGRATOR_ROLEback to the same address.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?