Lack of NFT ownership check on closePosition enables boost market exploitation
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.
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:
// NFT check only happens once at stake timeif (isNftHolder(msg.sender)) {boostedInterest = (baseInterest * rewardBoostMultiplier) / 100;}// Boosted rewards are permanently storedpositions[currentPositionId] = Position({tokensInterest: boostedInterest, // Locked at stake time// ...});// No NFT verification at withdrawalfunction 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.
Impact
The vulnerability undermines the entire NFT boost mechanism:
- Protocol loses ability to reward genuine long-term NFT holders.
- Single NFT can be used to boost multiple positions.
- Creates a secondary market that commoditises the boost feature.
- Economic impact equals
(boostedInterest - baseInterest)for each exploited position.
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.
struct Position {address walletAddress;uint createdDate;uint unlockDate;uint lockedDays;uint baseReward; // Store base rewarduint finalReward; // Store boosted reward if applicablebool initialNftHolder; // Track if user had NFT when stakinguint tokensStaked;bool open;}
In closePosition, use stored values with conditional logic:
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.
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.

