F-2025-0004·reentrancy

Reentrancy via Ownership Transfer Before Stake State Update in StakingPool

Fixedliquid-stakinglststaking-poolsgithub.com/matchain/contracts
TL;DR

_transferOwnership calls ownershipNFT.transferFrom (firing onERC721Received) before migrating stakes[newOwner] to selfStake, opening a reentrancy window where the new owner is recognised as the pool owner with stale stake balances.

Severity
HIGH
Impact
HIGH
Likelihood
MEDIUM
Method
MManual review
CAT.
Complexity
MEDIUM
Exploitability
MEDIUM
02Section · Description

Description

In StakingPool.sol, the _transferOwnership function is overridden to support ownership handoff via ERC721 transfer of the ownershipNFT token. However, the current implementation introduces a serious reentrancy vulnerability due to the incorrect ordering of operations.

Here is the function in question:

solidity
function _transferOwnership(address newOwner) internal virtual override {
address oldOwner = owner();
if (oldOwner == newOwner) return;
ownershipNFT.transferFrom(oldOwner, newOwner, uint160(address(this)));
if (stakes[newOwner] > 0) {
selfStake += stakes[newOwner];
stakes[newOwner] = 0;
}
emit OwnershipTransferred(oldOwner, newOwner);
}

The issue arises from the call to ownershipNFT.transferFrom(...), which occurs before updating the internal staking state via:

solidity
if (stakes[newOwner] > 0) {
selfStake += stakes[newOwner];
stakes[newOwner] = 0;
}

This ordering violates the checks-effects-interactions pattern because transferFrom may trigger arbitrary logic, particularly the onERC721Received hook, on the newOwner if it is a smart contract. At this point in execution:

  • The newOwner already owns the pool NFT.
  • The stakes[newOwner] balance is not yet migrated to selfStake.
  • The owner() has not been officially updated yet either (depending on how ownership is managed), potentially introducing inconsistencies.
03Section · Impact

Impact

A malicious contract receiving ownership can re-enter the StakingPool through onERC721Received while still holding the pre-migration stakes[newOwner] balance, allowing double-counted operations or unintended state reads.

04Section · Recommendation

Recommendation

  • Reorder the logic to update staking state before transferring the NFT:
solidity
function _transferOwnership(address newOwner) internal virtual override {
address oldOwner = owner();
if (oldOwner == newOwner) return;
// Migrate stake to selfStake before transferring NFT
if (stakes[newOwner] > 0) {
selfStake += stakes[newOwner];
stakes[newOwner] = 0;
}
ownershipNFT.transferFrom(oldOwner, newOwner, uint160(address(this)));
emit OwnershipTransferred(oldOwner, newOwner);
}
  • Add nonReentrant modifier.
F-2025-0004

oog
zealynx

Smart Contract Security Digest

Monthly exploit breakdowns, audit checklists, and DeFi security research — straight to your inbox

© 2026 Zealynx