Immunefi Audit Competitions
Folks Finance: Staking Contracts

69936 sc low users cannot revoke migration permits once the migrator s role has been revoked

Submitted on Mar 17th 2026 at 13:23:17 UTC by @Happy_Hunter for Audit Comp | Folks Finance: Staking Contracts

Description

Brief/Intro

The function only allows changing a migration permit when _migrator currently has MIGRATOR_ROLE. Once an admin revokes that role from a migrator, users who had approved them can no longer revoke the approval: setMigrationPermit(_migrator, false) reverts with MigratorNotFound.

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);
}

When a user had approved a migrator that held MIGRATOR_ROLE and the admin later revokes the role, the user's attempt to revoke their own permit (setMigrationPermit(_migrator, false)) reverts with MigratorNotFound. The storage entry migrationPermits[_migrator][user] = true is never cleared.

No other function modifies migrationPermits; the user has no alternative way to revoke the permit once the migrator has lost the role.

Affected code (root cause):

if (!hasRole(MIGRATOR_ROLE, _migrator)) revert MigratorNotFound(_migrator);

Impact Details

Contract fails to deliver promised returns, but doesn't lose value.

References

  • src/Staking.sol

Recommendation

Require _migrator to have MIGRATOR_ROLE only when the caller is setting a permit to true; when clearing a permit (false), skip the role check so users can revoke for any address.

if (_isMigrationPermitted) {
    if (!hasRole(MIGRATOR_ROLE, _migrator)) revert MigratorNotFound(_migrator);
}

Proof of Concept

PoC Code

Add the following tests to folks-staking-contracts/test/Staking.t.sol (same setup as existing tests: admin, alice, migrator, token, staking with MIGRATOR_ROLE granted to migrator in setUp()):

function test_PoC_UserCannotRevokePermitAfterMigratorRoleRevoked() public {
    vm.prank(alice);
    staking.setMigrationPermit(migrator, true);
    assertTrue(staking.migrationPermits(migrator, alice), "permit should be true");

    vm.startPrank(admin);
    staking.revokeRole(staking.MIGRATOR_ROLE(), migrator);
    vm.stopPrank();
    assertFalse(staking.hasRole(staking.MIGRATOR_ROLE(), migrator), "migrator should have no role");

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

    assertTrue(staking.migrationPermits(migrator, alice), "stale permit still true, user cannot revoke");
}

Test Result

Compiling 1 files with Solc 0.8.30
Solc 0.8.30 finished in 6.32s
Compiler run successful!

Ran 1 test for test/Staking.t.sol:StakingTest
[PASS] test_PoC_UserCannotRevokePermitAfterMigratorRoleRevoked() (gas: 82972)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.54ms (2.23ms CPU time)

Ran 1 test suite in 14.54ms (6.54ms CPU time): 1 test passed, 0 failed, 0 skipped (1 total tests)