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: interactionemitRecovered(tokenAddress, tokenAmount);// L160: effect after
In migratePositionsFrom (Staking.sol:206-208):
TOKEN.safeTransfer(msg.sender, unclaimedUserAmount + unclaimedUserRewards);// L206: interactionemitMigrateFrom(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
Manager calls recoverERC20(tokenAddress, amount) to recover excess tokens
safeTransfer emits an ERC20 Transfer event at log index N
Recovered event is emitted at log index N+1 (after the transfer)