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 afterIn migratePositionsFrom (Staking.sol:206-208):
TOKEN.safeTransfer(msg.sender, unclaimedUserAmount + unclaimedUserRewards); // L206: interaction
emit MigrateFrom(msg.sender, user); // L208: effect afterThe 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 eventsrc/Staking.sol:206-208—migratePositionsFrom: safeTransfer before MigrateFrom eventsrc/Staking.sol:294-295—_stake: correctly emits before safeTransferFromsrc/Staking.sol:328-329—_withdraw: correctly emits before safeTransfer
Proof of Concept
Manager calls
recoverERC20(tokenAddress, amount)to recover excess tokenssafeTransferemits an ERC20Transferevent at log index NRecoveredevent is emitted at log index N+1 (after the transfer)Similarly, migrator calls
migratePositionsFrom(user)safeTransferemitsTransferat log index MMigrateFromis emitted at log index M+1 (after the transfer)By contrast,
_stakeemitsStakedbeforeTransfer— 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?