Contract fails to deliver promised returns, but doesn't lose value
Description
Brief/Intro
The setMigrationPermit function enforces that the target address must currently hold MIGRATOR_ROLE for both granting and revoking permits. This means if an admin revokes a migrator's role, any user who previously granted that migrator a permit is permanently unable to revoke it, breaking the protocol's explicit documentation guarantee.
The migrator must hold the MIGRATOR_ROLE in the staking contract. The permission can be revoked at any time by calling setMigrationPermit(migratorAddress, false).
Vulnerability Details
setMigrationPermit checks the migrator role unconditionally:
The role check makes sense when granting a permit. You don't want users granting permits to random addresses. But when revoking, the check is unnecessary and harmful. If the admin revokes MIGRATOR_ROLE from a migrator, any user attempting to call setMigrationPermit(migrator, false) will revert with MigratorNotFound.
The protocol documentation explicitly states:
"The permission can be revoked at any time by calling setMigrationPermit(migratorAddress, false)."
Impact Details
The stale true permit persists in migrationPermits[migrator][user] mapping. If the admin ever re-grants MIGRATOR_ROLE to the same migrator address, that migrator can immediately call migratePositionsFrom(user) and migrate the user's positions, without the user's current consent, since they were unable to revoke during the window when the role was inactive.
References
Add any relevant links to documentation or code
Proof of Concept
Paste the following code snippet to staking.t.sol: