Immunefi Audit Competitions
Folks Finance: Staking Contracts

69814 sc low stale migration permits cannot be revoked after migrator role removal

Submitted on Mar 16th 2026 at 23:32:46 UTC by @incogknito for Audit Comp | Folks Finance: Staking Contracts

Description

Finding

The README says migration permission can be revoked at any time, but that is not true in Staking.sol: https://github.com/Folks-Finance/folks-staking-contracts/blob/3131a2d46b5afa76f606bf08adfd85452a47e2d8/src/Staking.sol#L77-L82

setMigrationPermit() requires the target address to currently hold MIGRATOR_ROLE. So if a user approves a migrator and that role is later revoked, calling setMigrationPermit(migrator, false) reverts with MigratorNotFound, leaving the old permit stored in migrationPermits.

If the same address receives MIGRATOR_ROLE again later, the old approval becomes usable again and migratePositionsFrom() can be called without any fresh user action.

Impact

Users cannot revoke a previously granted migration permit once the role is removed. The old permit remains onchain. Re-granting MIGRATOR_ROLE makes the stale approval usable again.

Recommendation

Allow setMigrationPermit(migrator, false) even if migrator no longer has MIGRATOR_ROLE.

Proof of Concept

function test_PoC_Info_MigrationPermitReactivatesAfterRoleRegrant() public {
    vm.prank(bob);
    staking.setMigrationPermit(migrator, true);
    assertEq(staking.migrationPermits(migrator, bob), true);

    vm.prank(admin);
    staking.revokeRole(staking.MIGRATOR_ROLE(), migrator);

    vm.expectRevert(abi.encodeWithSelector(IStakingV1.MigratorNotFound.selector, migrator));
    vm.prank(bob);
    staking.setMigrationPermit(migrator, false);

    assertEq(staking.migrationPermits(migrator, bob), true);

    vm.prank(admin);
    staking.grantRole(staking.MIGRATOR_ROLE(), migrator);

    vm.expectEmit(true, true, true, true);
    emit IStakingV1.MigrateFrom(migrator, bob);

    vm.prank(migrator);
    IStakingV1.UserStake[] memory migratedStakes = staking.migratePositionsFrom(bob);

    assertEq(migratedStakes.length, 0);
}

forge test --match-test test_PoC_Info_MigrationPermitReactivatesAfterRoleRegrant -vv