69870 sc insight events emitted after external calls in recovererc20 and migratepositionsfrom violate cei pattern

Submitted on Mar 17th 2026 at 07:43:59 UTC by @hyuunn for Audit Comp | Folks Finance: Staking Contracts

  • Report ID: #69870

  • Report Type: Smart Contract

  • Report severity: Insight

  • Target: https://github.com/Folks-Finance/folks-staking-contracts/blob/main/src/Staking.sol

  • Impacts:

Description

Brief/Intro

Two functions (recoverERC20 and migratePositionsFrom) emit their effect events after external safeTransfer calls, violating the Checks-Effects-Interactions (CEI) pattern. The same contract correctly follows CEI in _stake and _withdraw, demonstrating this is an oversight rather than a design choice.

Vulnerability Details

In recoverERC20 (Staking.sol:159-160), the external call executes before the event:

IERC20(tokenAddress).safeTransfer(msg.sender, tokenAmount);  // L159: interaction
emit Recovered(tokenAddress, tokenAmount);                    // L160: effect after

In migratePositionsFrom (Staking.sol:206-208):

TOKEN.safeTransfer(msg.sender, unclaimedUserAmount + unclaimedUserRewards);  // L206: interaction
emit MigrateFrom(msg.sender, user);                                          // L208: effect after

The same contract correctly follows CEI elsewhere:

Both recoverERC20 and migratePositionsFrom are protected by nonReentrant (migratePositionsFrom) or role-gated (recoverERC20), so this is not directly exploitable. However, the inconsistency violates the security best practice of uniform CEI ordering.

Impact Details

Event-driven off-chain systems that reconstruct contract state from transaction logs expect effects (events) to be recorded before interactions (external calls). The inconsistent ordering between functions in the same contract means off-chain indexers must handle two different event ordering patterns depending on which function was called.

References

  • src/Staking.sol:159-160recoverERC20: safeTransfer before Recovered event

  • src/Staking.sol:206-208migratePositionsFrom: safeTransfer before MigrateFrom event

  • src/Staking.sol:294-295_stake: correctly emits before safeTransferFrom

  • src/Staking.sol:328-329_withdraw: correctly emits before safeTransfer

Proof of Concept

  1. Manager calls recoverERC20(tokenAddress, amount) to recover excess tokens

  2. safeTransfer emits an ERC20 Transfer event at log index N

  3. Recovered event is emitted at log index N+1 (after the transfer)

  4. Similarly, migrator calls migratePositionsFrom(user)

  5. safeTransfer emits Transfer at log index M

  6. MigrateFrom is emitted at log index M+1 (after the transfer)

  7. By contrast, _stake emits Staked before Transfer — proving the contract's own standard is effects-before-interactions

Run: FOUNDRY_PROFILE=poc forge test --match-contract F_U1_005_Test -vvvv

Was this helpful?