F-2025-0003·missing-validation

Lack of NFT ownership check on closePosition enables boost market exploitation

Fixedstakingnft-boostrewards
TL;DR

Boosted rewards are locked into the position struct at stake time without re-validation at withdrawal, enabling rental markets where a single NFT boosts many positions.

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

Description

The StakingContract only checks NFT ownership at stake time and permanently stores the boosted rewards in the position struct. This allows for the creation of NFT rental markets specifically designed to exploit the boost mechanism:

solidity
// NFT check only happens once at stake time
if (isNftHolder(msg.sender)) {
boostedInterest = (baseInterest * rewardBoostMultiplier) / 100;
}
// Boosted rewards are permanently stored
positions[currentPositionId] = Position({
tokensInterest: boostedInterest, // Locked at stake time
// ...
});
// No NFT verification at withdrawal
function closePosition(uint positionId) external nonReentrant {
uint totalTokens = position.tokensStaked + position.tokensInterest;
stakingToken.safeTransfer(msg.sender, totalTokens);
}

This design enables the creation of centralised "boost-as-a-service" markets where:

  • NFT holders can rent out their NFTs for brief moments.
  • Stakers can rent NFTs just for the staking transaction.
  • Multiple stakers can use the same NFT to get boosted rewards.
03Section · Impact

Impact

The vulnerability undermines the entire NFT boost mechanism:

  1. Protocol loses ability to reward genuine long-term NFT holders.
  2. Single NFT can be used to boost multiple positions.
  3. Creates a secondary market that commoditises the boost feature.
  4. Economic impact equals (boostedInterest - baseInterest) for each exploited position.
04Section · Recommendation

Recommendation

During mitigation period, an improved approach was identified for handling NFT boosts. Store both the base and boosted reward values in the Position struct, along with tracking the initial NFT ownership status, then determine the final reward at close based on whether the staker still holds the NFT.

solidity
struct Position {
address walletAddress;
uint createdDate;
uint unlockDate;
uint lockedDays;
uint baseReward; // Store base reward
uint finalReward; // Store boosted reward if applicable
bool initialNftHolder; // Track if user had NFT when staking
uint tokensStaked;
bool open;
}

In closePosition, use stored values with conditional logic:

solidity
uint finalReward = (isNftHolder(msg.sender) && pos.initialNftHolder)
? pos.finalReward
: pos.baseReward;

This implementation ensures users who had NFTs when staking and still have them at withdrawal get the boosted reward; users who had NFTs when staking but no longer have them get the base reward; users who did not have NFTs when staking but acquired them later do not get retroactively boosted.

05Section · Resolution

Resolution

Ample Protocol: Fixed.

Zealynx: The team implemented the recommendation to track NFT ownership; an additional optimisation was identified during verification and the recommendation was updated. Suggestion implemented and verified.

Status
Fixed
F-2025-0003

oog
zealynx

Smart Contract Security Digest

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

© 2026 Zealynx