For the complete documentation index, see llms.txt. This page is also available as Markdown.

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-160 — recoverERC20: safeTransfer before Recovered event

  • src/Staking.sol:206-208 — migratePositionsFrom: 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?