60149 sc insight revised missing input validation in addlevels can break multiple staking tier invariant in startgatenft
Submitted on Nov 19th 2025 at 09:04:53 UTC by @blackgrease for Audit Comp | Vechain | Stargate Hayabusa
Report ID: #60149
Report Type: Smart Contract
Report severity: Insight
Target: https://github.com/immunefi-team/audit-comp-vechain-stargate-hayabusa/tree/main/packages/contracts/contracts/StargateNFT/libraries/Levels.sol
Summary
Affected files: StargateNFT.sol and Levels.sol
This is reported as an Insight under "Code Optimizations and Enhancements / Security Best Practices". The core issue: the Levels::addLevel function lacks sufficient input validation to enforce several invariants implied by the StargateNFT documentation and intended protocol behavior. As of StargateNFT v3 the updateLevel function is no longer available, so any incorrect configuration added via addLevel is effectively permanent unless the contract is upgraded.
Invariants (from documentation and intended behavior)
Each staking tier must have a unique id.
Each staking tier must have a unique name.
Each staking tier has its own VET staking amount.
Each staking tier must have its own/unique reward factor. (Docs: "Each NFT tier is associated with a reward multiplier that enhances the user's share of rewards")
Each staking tier must have a different maturity period. (Docs: "Every NFT level has a different maturity period,")
A maturity period cannot be 0. (Docs: "Every staking NFT is subject to a maturity period, which is determined by its node tier.")
All staking tiers must have a cap. (Docs: "All NFT levels have a cap...")
What is missing
Levels::addLevel only validates:
level.name is not empty
vetAmountRequiredToStake is not 0 (for non-zero level)
cap vs circulatingSupply consistency (circulatingSupply must not exceed cap)
It does NOT validate:
vetAmountRequiredToStake uniqueness across levels
scaledRewardFactor (non-zero, uniqueness, or reasonable bounds)
maturityBlocks (non-zero, uniqueness across levels, or reasonable upper bound)
optionally: uniqueness of level names (affects UI/UX)
Because updateLevel is removed in v3, these misconfigurations cannot be corrected through on-chain functions after submission of a bad level.
Breaking the invariants — Examples & Effects
Below are concrete cases that illustrate how lack of validation can break protocol expectations:
Case #1: Staking tiers with same VET staking amount
addLevel does not check if the new level's VET stake amount matches an existing level.
If a new level requires the same VET amount but has a higher reward factor, stakers can earn greater rewards for the same stake — undermining tier semantics and fairness.
Case #2: No validation for level.scaledRewardFactor
Duplicate reward factor across different stake amounts: cheaper tiers could offer same rewards as more expensive tiers.
Zero reward factor: a tier could be created that grants zero rewards.
Case #3: No validation for level.maturityBlocks
Zero maturity: maturityBlocks could be 0, allowing immediate delegation/minting contrary to docs.
Excessively large maturity: no upper bound, allowing unintentionally huge maturity periods (e.g., an extra zero turns 60 days into ~600 days).
Duplicate maturity periods: multiple tiers could share the same maturity period, contradicting "Every NFT level has a different maturity period".
Relevant code excerpts
StargateNFT::addLevel (simplified):
_validateLevel:
Impact
This is reported as an Insight (not a formal vulnerability) because the consequence is a misconfiguration that can damage staking semantics, UX, and protocol intent. Missing validation is a security best practice issue: it’s advisable to validate inputs that make permanent changes to contract state, especially when on-chain updates are irreversible in the deployed implementation.
Potential impacts:
Economic unfairness (lower-tier users earning rewards meant for higher tiers).
Broken UX due to duplicate names or tiers with unexpected properties.
Permanently incorrect tier configuration requiring an upgrade to fix.
Mitigation / Recommendations
Add additional validation in
addLevelto enforce the invariants before persisting a new level:Ensure vetAmountRequiredToStake is unique across existing levels (or otherwise ensure tiers are distinctly ordered).
Ensure scaledRewardFactor is non-zero and within reasonable bounds; optionally ensure uniqueness.
Ensure maturityBlocks is non-zero, within a reasonable upper bound, and unique across levels.
Optionally ensure name uniqueness to avoid UI/UX confusion.
Re-introduce an
updateLevelfunction (with appropriate admin controls and constraints) so that legitimate mistakes can be rectified without a full contract upgrade.Consider adding events and off-chain checks/tests in deployment tooling to validate tier sets before calling
addLevel.
Proof of Concept
A Foundry-compatible PoC is provided in a gist that demonstrates creating levels that break these invariants:
PoC Gist: https://gist.github.com/blackgrease/87a7070799eaea0aecabf892a4e0d9f0
Run with:
PoC description:
Case_#1: Demonstrates duplicate stake amounts, duplicate maturity periods and a reward factor of 0 in one level.
Case_#2: Demonstrates duplicate stake amounts with different reward factors.
Case_#3: Demonstrates lack of maximum maturityBlocks (e.g., an unintended extra zero leads to ~600 days) and duplicate reward factors.
Note: the test suite was originally Hardhat-based; conversion instructions are below.
Hardhat -> Foundry conversion (how to run PoC)
Additional notes
The report treats this as an Insight: the root cause is insufficient input validation on a function that performs a permanent state change (and the removal of an
updateLevelfunction exacerbates the risk).The suggested mitigations are conservative: enforce invariants in
addLeveland consider reintroducing controlled update functionality to allow safe correction of accidental misconfigurations.
-- End of report.
Was this helpful?