> For the complete documentation index, see [llms.txt](https://reports.immunefi.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://reports.immunefi.com/folks-finance-staking-contracts/69376-sc-low-incorrect-guard-in-setmigrationpermit-prevents-revocation-after-role-removal-breaking-d.md).

# 69376 sc low incorrect guard in setmigrationpermit prevents revocation after role removal breaking documented user control

**Submitted on Mar 14th 2026 at 14:33:33 UTC by @Orhuk1 for** [**Audit Comp | Folks Finance: Staking Contracts**](https://immunefi.com/audit-competition/audit-comp-folks-finance-staking-contracts)

* **Report ID:** #69376
* **Report Type:** Smart Contract
* **Report severity:** Low
* **Target:** <https://github.com/Folks-Finance/folks-staking-contracts/blob/main/src/Staking.sol>
* **Impacts:**
  * Contract fails to deliver promised returns, but doesn't lose value

## Description

The `setMigrationPermit` function applies the `MIGRATOR_ROLE` check unconditionally regardless of whether the caller is granting or revoking a permit:

```solidity
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 migrator's role is revoked by the admin, any user who had previously granted that migrator a permit can no longer revoke it. The call reverts with `MigratorNotFound` even though the user is only attempting to modify their own state.

This directly contradicts the documented behavior in the README:

> *"The permission can be revoked at any time by calling setMigrationPermit(migratorAddress, false)."*

The fix is to apply the role guard only when granting, not when revoking:

```solidity
function setMigrationPermit(address _migrator, bool _isMigrationPermitted) external {
    if (_isMigrationPermitted && !hasRole(MIGRATOR_ROLE, _migrator)) {
        revert MigratorNotFound(_migrator);
    }
    migrationPermits[_migrator][msg.sender] = _isMigrationPermitted;
    emit MigrationPermitUpdated(_migrator, msg.sender, _isMigrationPermitted);
}
```

This preserves the behaviour that users cannot grant permits to non-migrators, while allowing unconditional revocation regardless of the migrator's current role status.

## Proof of Concept

**Steps:**

1. Alice grants a migration permit to `migrator` while the role is active
2. Admin revokes `MIGRATOR_ROLE` from `migrator`
3. Alice calls `setMigrationPermit(migrator, false)` to revoke her permit
4. The call reverts with `MigratorNotFound` — Alice has no on-chain remedy

```solidity
function test_revocationBlockedAfterRoleRemoved() public {
    deal(address(token), address(staking), 1000 ether);
    deal(address(token), alice, 100 ether);

    uint8 periodIndex = addStakingPeriodByManager(50 ether, 20, 10, 5000, true);
    approveAndStake(alice, periodIndex, 10 ether, 20, 10, 5000, address(0));

    // Step 1: Alice grants permit to migrator
    vm.prank(alice);
    staking.setMigrationPermit(migrator, true);
    assertTrue(staking.migrationPermits(migrator, alice));

    // Cache role before pranking to prevent prank being consumed by argument evaluation
    bytes32 migratorRole = staking.MIGRATOR_ROLE();

    // Step 2: Admin revokes migrator role
    vm.prank(admin);
    staking.revokeRole(migratorRole, migrator);
    assertFalse(staking.hasRole(migratorRole, migrator));

    // Step 3 & 4: Alice attempts to revoke her permit — reverts
    vm.expectRevert(
        abi.encodeWithSelector(IStakingV1.MigratorNotFound.selector, migrator)
    );
    vm.prank(alice);
    staking.setMigrationPermit(migrator, false);

    // Permit remains true with no on-chain remedy for Alice
    assertTrue(staking.migrationPermits(migrator, alice));
}
```

```bash
forge test --match-test test_revocationBlockedAfterRoleRemoved -vvv
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://reports.immunefi.com/folks-finance-staking-contracts/69376-sc-low-incorrect-guard-in-setmigrationpermit-prevents-revocation-after-role-removal-breaking-d.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
