69031 sc low user cannot revoke permission from migrator if it does not have migrator role
Submitted on Mar 12th 2026 at 12:41:30 UTC by @Audittens for Audit Comp | Folks Finance: Staking Contracts
Report ID: #69031
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
Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
Description
Brief/Intro
Function Staking::setMigrationPermit requires _migrator to have MIGRATOR_ROLE even when a user attempts to revoke permission. This violates the promise: "The permission can be revoked at any time by calling setMigrationPermit(migratorAddress, false)".
Moreover, this behavior enables a malicious admin to steal users' funds.
Vulnerability Details
Function Staking::setMigrationPermit(address _migrator, bool _isMigrationPermitted) allows users to grant (_isMigrationPermitted = true) or revoke (_isMigrationPermitted = false) permission to migrate their positions using _migrator. However, in both cases it requires hasRole(MIGRATOR_ROLE, _migrator).
Consider the following scenario:
Admin grants
MIGRATOR_ROLEto Migrator.User grants migration permission to Migrator.
Admin revokes
MIGRATOR_ROLEfrom Migrator.User attempts to revoke permission from Migrator but cannot — this violates the promise: "The permission can be revoked at any time by calling
setMigrationPermit(migratorAddress, false)".
Additionally, this issue can allow a malicious admin to steal users' funds. Consider the following scenario:
Admin deploys an upgradeable Migrator contract with a good implementation and the ability to upgrade the implementation with a 30-day timelock.
Admin grants
MIGRATOR_ROLEto the Migrator.User grants permission to the Migrator, trusting the timelock: if the admin upgrades the Migrator, the user expects to have 30 days to revoke permission.
Admin revokes
MIGRATOR_ROLEfrom the Migrator and schedules an upgrade to a malicious implementation that steals users' funds.The user observes this upgrade but cannot revoke permission, because the Migrator no longer has
MIGRATOR_ROLE.After 30 days, the admin atomically grants
MIGRATOR_ROLEback to the Migrator, completes the upgrade, and steals the user's funds via the malicious Migrator implementation.
Impact Details
There are two impacts:
1. [Critical] Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
A malicious admin can steal users' funds.
Note that this attack requires a malicious admin, but it is still in scope, because by design the admin only has the privilege to:
authorize potential migrators
but does not have privileges to:
prevent users from revoking their permissions, or
steal users' funds.
Relevant program rules:
Default Out of Scope and rules Impacts caused by attacks requiring access to privileged addresses (including, but not limited to: governance and strategist contracts) without additional modifications to the privileges attributed
What addresses would you consider any bug report requiring their involvement to be out of scope, as long as they operate within the privileges attributed to them? Default admin, manager, pauser, migrator.
2. [Low] Contract fails to deliver promised returns, but doesn't lose value
The contract violates the documented guarantee: "The permission can be revoked at any time by calling setMigrationPermit(migratorAddress, false)".
Importantly, this issue does not require a malicious admin.
References
https://github.com/Folks-Finance/folks-staking-contracts/blob/3131a2d/src/Staking.sol#L78
https://github.com/Folks-Finance/folks-staking-contracts/blob/3131a2d/README.md?plain=1#L140C72-L140C169
Proof of Concept
The following codes simulate the scenarios described in the Vulnerability Details section.
Save them to files src/test/PoC_1_AdminStealsFunds.t.sol and src/test/PoC_2_UnsetPermitFails.t.sol respectively. Then run: forge test --mc PoC* -vv. This will produce the following logs:
PoC for impact 1 (Critical)
PoC for impact 2 (Low)
Was this helpful?