Milestone HODL Token Info
Milestone HODL Token (MHT) is a decentralized protocol on the BNB Smart Chain engineered to transform market growth into direct, automated rewards for holders. By integrating Chainlink Automation technology, MHT eliminates human intervention, ensuring token distribution is strictly based on real-time Market Cap performance.
TrustNet Score
The TrustNet Score evaluates crypto projects based on audit results, security, KYC verification, and social media presence. This score offers a quick, transparent view of a project's credibility, helping users make informed decisions in the Web3 space.
Real-Time Threat Detection
Real-time threat detection, powered by Cyvers.io,
is currently not
activated
for this project.
This advanced feature provides continuous monitoring and instant alerts to safeguard your assets from potential security threats. Real-time detection enhances your project's security by proactively identifying and mitigating risks.
For more information, click here.
Summary and Final Words
No crucial issues found
The contract does not contain issues of high or medium criticality. This means that no known vulnerabilities were found in the source code.
Contract owner cannot mint
It is not possible to mint new tokens.
Contract owner cannot blacklist addresses.
It is not possible to lock user funds by blacklisting addresses.
Contract owner cannot set high fees
The fees, if applicable, can be a maximum of 25% or lower. The contract can therefore not be locked. Please take a look in the comment section for more details.
Contract cannot be locked
Owner cannot lock any user funds.
Token cannot be burned
There is no burning within the contract without any allowances
Ownership is renounced
The contract does not include owner functions that allow post-deployment modifications.
Contract is not upgradeable
The contract does not use proxy patterns or other mechanisms to allow future upgrades. Its behavior is locked in its current state.
Scope of Work
This audit encompasses the evaluation of the files listed below, each verified with a SHA-1 Hash. The team referenced above has provided the necessary files for assessment.
The auditing process consists of the following systematic steps:
- Specification Review: Analyze the provided specifications, source code, and instructions to fully understand the smart contract's size, scope, and functionality.
- Manual Code Examination: Conduct a thorough line-by-line review of the source code to identify potential vulnerabilities and areas for improvement.
- Specification Alignment: Ensure that the code accurately implements the provided specifications and intended functionalities.
- Test Coverage Assessment: Evaluate the extent and effectiveness of test cases in covering the codebase, identifying any gaps in testing.
- Symbolic Execution: Analyze the smart contract to determine how various inputs affect execution paths, identifying potential edge cases and vulnerabilities.
- Best Practices Evaluation: Assess the smart contracts against established industry and academic best practices to enhance efficiency, maintainability, and security.
- Actionable Recommendations: Provide detailed, specific, and actionable steps to secure and optimize the smart contracts.
A file with a different Hash has been intentionally or otherwise modified after the security review. A different Hash may indicate a changed condition or potential vulnerability that was not within the scope of this review.
Final Words
The following provides a concise summary of the audit report, accompanied by insightful comments from the auditor. This overview captures the key findings and observations, offering valuable context and clarity.
Smart Contract Analysis Statement
Contract Analysis
The Milestone HODL Token (MHT) contract implements a BEP-20 token on BNB Smart Chain with milestone-driven holder rewards, automatic LP tranches, a flush-LP schedule on price tiers, Chainlink Automation, buy and sell taxes, an anti-whale per-transaction cap, a hardcoded V1 snapshot airdrop with mirror burn, and a deliberate trustless transition through renounceOwnership. V2.3 is a cosmetic-only revision over V2.2_corrected and applies the three remaining stylistic items from the prior audit (I-16 modifier ordering on rescueTokens, I-17 marketing-BNB observability event, I-18 dead _swapping toggle removal). All HIGH and MEDIUM items from earlier rounds are resolved or accepted as documented design trade-offs. A few residual items still need attention before mainnet:
- Spot-price reads in getMarketCap and getMHTPrice still gate milestone, LP-tranche and flush-LP releases. The team has accepted this as a design trade-off; the mitigation rests on five layers (registry caller restriction, 1 percent maxTx, 7 percent round-trip taxes, LP tokens routed to burn, and the one-tranche-per-upkeep cap). A multi-block adversarial-timing surface remains and is documented in NatSpec.
- The blacklist primitive is bounded to a 7-day post-deploy window and a 32-address cap. After day 7 the function reverts unconditionally, including for un-blacklisting, so the entire list must be reviewed before the window closes. This should be a hard deadline in the launch checklist.
- The catch branch of the auto-LP path discards the success boolean of the marketing-wallet BNB salvage. Pre-renounce these BNB amounts are recoverable through rescueTokens. Post-renounce, if the marketing wallet ever rejects native BNB, the value would be silently stranded. A 1-wei native-BNB probe of the marketing wallet between initLiquidity and renounceOwnership is the recommended off-chain mitigation and should be a hard prerequisite for renounce.
Ownership Privileges
The ownership of the contract has not yet been renounced - the deploy plan calls for renounceOwnership() after the live configuration is verified. Until that step, the owner retains a meaningful admin surface. After renounce, the contract becomes fully trustless and every privilege below is permanently removed.
- Set the PancakeSwap pair address and initialise 30M MHT plus 10 BNB of starting liquidity (one-shot through initLiquidity).
- Set or change the Chainlink Automation registry. Setting the registry is mandatory before renounceOwnership can succeed; the same applies to liquidity initialisation.
- Maintain the blacklist (redirect-to-burn) within the 7-day window and the 32-address cap, and grant or revoke per-address exemptions from taxes, the per-transaction cap, and the rewards distribution.
- Adjust the per-transaction cap within hard bounds of 0.1 percent and 10 percent of total supply, and rescue accidentally-sent tokens or BNB (for MHT, strictly bounded to the contract balance not already tracked).
- After renounce, no party can change the pair, the registry, the blacklist, the exclusion sets, the per-transaction cap, or rescue stuck assets.
- The owner cannot mint new tokens.
- The owner cannot pause transfers or freeze user balances arbitrarily (the blacklist is windowed and capped).
- The owner cannot drain user-held tokens or affect the milestone reward accounting of any holder.
Security Features
The contract implements several positive security features:
- ReentrancyGuard with nonReentrant on every privileged or value-moving entry point (initLiquidity, performUpkeep, claimRewards, rescueTokens), and a separate _swapping latch that suppresses tax and anti-whale logic during the contract's own internal swaps.
- Strict Chainlink oracle validation (roundId, answer, updatedAt, answeredInRound) with a 600-second staleness window, roughly ten times the BNB/USD heartbeat on BSC. Registry-restricted performUpkeep, a 24-hour milestone cooldown, and a one-tranche-per-upkeep cap on both LP-MCap and flush-price paths prevent multi-tranche cascades.
- Post-constructor allocation invariant that asserts balanceOf(this) equals the sum of all tracked balances, plus a renounce-time dual guard requiring both a configured automation registry and an initialised liquidity pool. The operational brick scenario that would have frozen roughly 80 percent of the supply if renounce was called out of order is blocked on chain.
- Router refund on the auto-LP success path is captured and forwarded to the marketing wallet, observable through a dedicated MarketingBnbForwarded event with discriminated source codes for all three native-BNB-to-marketing paths. Eligible-supply denominator excludes the contract, burn address, pair and marketing wallet so the milestone rate reflects claimable holders only, and LP tokens minted during auto-liquidity are routed directly to the burn address.
Note - This audit report consists of a security analysis of the Milestone HODL Token smart contract. This analysis did not include economic analysis of the contract's tokenomics. Moreover, we only audited the main contract for the Milestone HODL Token team. Other contracts associated with the project were not audited by our team. We recommend investors do their own research before investing.
Files and details
Functions
public
/
State variables
public
/
Total lines
of code
/
Capabilities
Hover on items
/
Findings and Audit result
high Issues | 2 findings
Resolved
#1 high Issue
Constructor founderBonus Calculation Over-Counted flushLpBalance
The previous fix that introduced the BONUS pro-rata distribution added six BONUS constants and six super._update calls but failed to subtract the snapshot total from the founderBonus formula. Tracked balances therefore exceeded the actual contract balance by exactly the snapshot total of 4,300,573.505500874925621347 MHT. flushLpBalance was overstated by exactly that amount. Operational impact would have been that the sixth flush tranche, which sweeps the whole remaining flushLpBalance, would have triggered an insufficient-balance revert and the state machine would have been permanently stuck.
Resolved
#2 high Issue
_autoAddLiquidity Did Not Reset Tranche Flag When addLiquidityETH Reverts
_autoAddLiquidity pre-decremented lpBalance or flushLpBalance before the external swap and addLiquidity calls. On swap failure (bnbReceived == 0) the flag and balance were correctly restored. But on addLiquidityETH failure, the catch block only reset the _swapping flag; the tranche flag remained true and the tracked balance remained decremented even though only the half that went through the swap had actually left the contract. The other half MHT and the bnbReceived BNB were untracked inside the contract. After renounceOwnership() they would have been permanently stranded. The failure mode was highly likely with the previous 5% slippage on amountETHMin.
medium Issues | 5 findings
Resolved
#1 medium Issue
_swapAndDistribute Stranded keepForLP MHT When addLiquidityETH Reverts
Inside _swapAndDistribute, autoLpBuffer and marketingBuffer were cleared before the addLiquidityETH attempt. If addLiquidityETH reverted (caught by try/catch), the BNB was salvaged into bnbForMkt and forwarded to marketing, but the keepForLP MHT (equal to autoLpBuffer/2) had already been implicitly deducted by the buffer reset. That MHT remained in the contract but was no longer tracked anywhere. Pre-renounce the loss was recoverable, post-renounce it would have been permanent. Cumulative under volatile market conditions where the slippage band is repeatedly tripped.
Resolved
#2 medium Issue
setBlacklistBatch Was an Irreversible-After-Renounce Confiscation Primitive
Pre-renounce, the owner could mark any address with isRedirectToBurn = true. Once set, every transfer out of that address was silently converted into a burn: taxes were skipped, anti-whale was skipped, _updateReward(from) was called, then super._update(from, BURN_ADDRESS, value). The user retained the balance only nominally; any movement burned it. Because the owner could blacklist a holder immediately before renounceOwnership(), the blacklist became permanent. This was documented as silent anti-scam but represented a confiscation primitive and a centralisation risk that did not exist in earlier versions.
Resolved
#3 medium Issue
automationRegistry Permanently Zero After Renounce Freezes All Milestones, LP and Flush
performUpkeep required automationRegistry != address(0) and msg.sender == automationRegistry. If the deployer forgot to setAutomationRegistry before renounceOwnership, the registry would be permanently zero and no one could call performUpkeep. The full vaultBalance, lpBalance and flushLpBalance would have become permanently unreleasable. There was no recovery path post-renounce. This was a one-line operational mistake that could have bricked roughly 76% of the supply forever.
Resolved
#4 medium Issue
Aggressive bnbReceived/4 Slippage Strands BNB Refunded by addLiquidityETH
_autoAddLiquidity uses amountETHMin = bnbReceived/4 to allow up to 75% slippage on the BNB side. This was needed because a self-induced sell shifts the pool ratio, so pairing the full otherHalf MHT only consumes a fraction of the BNB just received. PancakeSwap V2 router refunds msg.value - amountETH back to msg.sender, which is the contract. The MHT contract has receive() and accepts the refund. That BNB is not credited to any tracked accounting variable, so post-renounce the only path to send BNB out of the contract is the same _autoAddLiquidity / _swapAndDistribute flow which uses the freshly-received delta and never touches the historical refund.
Resolved
#5 medium Issue
renounceOwnership Should Also Require liquidityInitialized
The custom renounceOwnership override correctly guards against a missing automation registry but does not guard against a missing initial liquidity. If renounce is called before initLiquidity, startLpBalance is locked, the pair has no reserves, all price-derived functions revert, and performUpkeep cannot fire. The contract is permanently bricked and roughly 80% of the supply (start LP, vault, LP and flush balances) is frozen forever, with no recovery path because all owner functions are dead.
low Issues | 8 findings
Resolved
#1 low Issue
Unused Return From Low-Level Call Allows Silent BNB Loss
Solc 0.8.34 emits 'unused local variable' warnings for several (bool sent, ) = payable(addr).call{value: x}('') sites. If the receiver is a contract whose receive or fallback reverts, the returned boolean is false but no error is propagated. In initLiquidity this could leave fragments of the deposited BNB stranded. In _swapAndDistribute every successful sell could silently leak BNB to a misconfigured marketing wallet. Pre-renounce the BNB is recoverable via rescueTokens, post-renounce it would be permanently stranded.
Resolved
#2 low Issue
excludeFromRewards Allows Setting Self-Inclusion of address(this)
_updateReward always returns early for account == address(this). If the owner mistakenly called excludeFromRewards(address(this), false) to 'include' the contract, the function would silently update userRewardPerTokenPaid and clear the exclusion flag, but _updateReward would still skip subsequent updates. The contract's own balance could therefore never accumulate pending rewards, but the owner would be led to believe it could.
Resolved
#3 low Issue
BNB/USD Oracle Staleness Window Too Generous
The previous version accepted a Chainlink answer up to 60 minutes stale. The BSC BNB/USD aggregator updates on a 0.5% deviation or every 60 seconds, so the previous threshold allowed a price 60x more stale than normal heartbeat. Combined with the 24h MILESTONE_COOLDOWN this was benign for milestones, but the LP-tranche and flush-LP paths could fire repeatedly within that window with a stale BNB price.
Resolved
#4 low Issue
High Cyclomatic Complexity in _resetTrancheFlag
_resetTrancheFlag uses an 11-arm if/else dispatch that toggles tranche flags and adjusts lpBalance or flushLpBalance based on a tranche id. The hand-rolled dispatch is hard to review, easy to break with a future tranche-id change, and is repeatedly flagged by static analysis with cyclomatic complexity 12.
Resolved
#5 low Issue
performUpkeep Lacks nonReentrant
performUpkeep chains _executeMilestone -> _swapAndDistribute -> addLiquidityETH/swap and _checkAndReleaseLPTranches -> _autoAddLiquidity -> swap, with no nonReentrant modifier in V2.1.1. Re-entry was bounded by the strict automationRegistry caller check and by the _swapping boolean inside _update. If automationRegistry is ever set to a multicall-style relay, an EOA, or a contract that can be tricked into re-entering, the _swapping boolean alone would be insufficient.
Acknowledged
#6 low Issue
Silent BNB Loss in _autoAddLiquidity Catch Path
Inside the _autoAddLiquidity catch branch, the BNB salvage path does (bool okCatch, ) = payable(marketingWallet).call{value: bnbReceived}(''); okCatch; which intentionally drops the return value to silence the unused-variable warning. If the marketing destination ever rejects the transfer, the BNB sits inside the contract balance with no tracking variable. After renounceOwnership() the rescueTokens function becomes unreachable, so the BNB cannot be recovered through any normal flow.
Acknowledged
#7 low Issue
Marketing-Wallet BNB Acceptance Is an Unverified Deploy-Time Invariant
After the M-04 (V22-M-01) fix in V2.2_corrected, three code paths can route native BNB to marketingWallet. _swapAndDistribute uses require on the success boolean (loud failure). The two _autoAddLiquidity paths intentionally discard the boolean (silent failure). This asymmetric trust model is acceptable pre-renounce because a misconfigured wallet would be detected immediately by failed sells, but post-renounce a marketingWallet that cannot receive native BNB will continue to silently strand value into the contract address with no event and no recovery path.
Acknowledged
#8 low Issue
Spot-Price getMarketCap and getMHTPrice Manipulable Under Adversarial Timing
getMarketCap and getMHTPrice derive their value from IPancakePair.getReserves() with no time-weighting. performUpkeep re-reads them at execution time and gates all milestone, LP-tranche and flush-LP releases on those spot values. The atomic single-transaction attack is not exploitable because performUpkeep is gated by the automation registry caller check. The realistic threat is multi-block adversarial timing: an attacker holds a manipulated price across the keeper polling window so checkUpkeep returns true on a manipulated reading and the actual performUpkeep transaction lands while the spike is still active. The 24h milestone cooldown limits the milestone path but does not protect the LP-tranche or flush paths.
informational Issues | 18 findings
Resolved
#1 informational Issue
Stale Comment on flushLpBalance Pool Size
The constructor comment block previously cited 65,666,421.560 MHT or 45,699,426.494 MHT figures that did not match the executed code. After the H-04 fix the correct value is 61,706,452.560770 MHT and the comment must reflect that.
Resolved
#2 informational Issue
First Milestone Bypassed Cooldown Because lastMilestoneTimestamp Was Zero
lastMilestoneTimestamp was left at the storage default of zero. The cooldown check block.timestamp >= 0 + MILESTONE_COOLDOWN was always true at deploy, so the first milestone had no cooldown protection. Combined with the spot-price oracle behaviour, this could have allowed a thin-liquidity manipulation at the very first eligible block to fire the first milestone, the first LP tranche and the first flush tranche together.
Resolved
#3 informational Issue
pendingRewardsOf Uses Live balanceOf and Latent Component Drifts on Transfer
pendingRewardsOf returns pendingRewards[account] plus (balanceOf(account) * delta) / 1e18. If a user transfers all tokens out before claiming, _updateReward(from) inside _update computes the latent component at the old balance and crystallises it correctly, but a front-end that read pendingRewardsOf before the transfer and then re-reads after will see the same crystallised value plus a newly-zero latent component, which can look like a jump to the user. This is the standard staking pattern and is correct on-chain.
Resolved
#4 informational Issue
_executeFlush Per-Tranche Math Depends on Cumulative Balance
Per-tranche amount is flushLpBalance / (7 - trancheNumber) for tranches 1 through 5 and flushLpBalance for tranche 6. The math is sound only if tranches fire in strict sequence and flushLpBalance accurately reflects the contract holdings. With the H-04 fix in place this holds, but the invariant is worth keeping under test.
Resolved
#5 informational Issue
Pre-Renounce Owner Can Permanently Grant Tax and maxTx Exclusions
excludeFromFees and excludeFromMaxTx allow the owner to grant any address tax-free or unlimited-tx privileges. Pre-renounce only, but a malicious or compromised deployer could grant these privileges to an attacker wallet then renounce, leaving the attacker with permanent immunity. Not a vulnerability in the cryptographic sense, just a governance and due-diligence note.
Resolved
#6 informational Issue
Hardcoded V1 Snapshot Recipient Addresses Must Be Verified
Six hardcoded addresses receive the V1 snapshot and bonus prorata at deploy. ERC-20 transfer to a contract whose receive reverts does not revert (no callback), but if any of the six addresses is a self-destructed contract or a now-deprecated multisig whose threshold can no longer be reached, the tokens are unreachable on-chain. The audit cannot verify ownership of those addresses and depends on the project team's due diligence.
Resolved
#7 informational Issue
V1_BURN_MIRROR Constant Cannot Be Verified Without V1 Address
V1_BURN_MIRROR was presented as the exact V1 burn balance at snapshot time. If the V1 burn balance changed between the snapshot block and the V2.1 deploy block, the mirror would be stale. Pinning the V1 address in source allows mechanical verification.
Resolved
#8 informational Issue
encodeWithSelector Replaced With encodeCall for Type Safety
rescueTokens previously used encodeWithSelector for the IERC20.transfer call to an arbitrary token. encodeCall is the modern type-safe alternative and is preferred to catch signature mismatches at compile time.
Pending
#9 informational Issue
Stylistic Static-Analysis Findings Roll-Up
Aggregate roll-up of low-impact static-analysis findings observed in V2.3: use-custom-error-not-require, large-numeric-literal, use-nested-if, inefficient-state-variable-increment, use-multiple-require, init-variables-with-default-value, naming-convention on the external IPancakeRouter02.WETH() function (cannot change), and dangerous strict equality on bnbReceived == 0 (intentional zero-detection of swap failure).
Resolved
#10 informational Issue
Constructor _swapping Toggle Around super._update Was Dead Code
The previous constructor set _swapping = true around super._update calls. Because those used super._update (the ERC-20 base) and not the override that gates on _swapping, the flag had no effect. The dead code mis-led readers about constructor behaviour.
Resolved
#11 informational Issue
rescueTokens Lacked nonReentrant Modifier
rescueTokens executes a low-level call against an arbitrary token contract address. A malicious token could re-enter into other onlyOwner functions during its transfer. No privileged user-state could be re-entered to drain user funds because everything else is onlyOwner anyway, but defence-in-depth was free.
Resolved
#12 informational Issue
Cascading Tranche Releases in a Single performUpkeep
When the market cap or the spot price jumps far above several thresholds at once, performUpkeep can fire all eligible LP and flush tranches in the same transaction. Each tranche moves the pool, so the next tranche fires against a price that has already absorbed the previous tranche's impact. There is no cap on the cascade.
Resolved
#13 informational Issue
Blacklisted Addresses Can Claim Rewards But Cannot Use Them
claimRewards blocks excluded-from-rewards addresses but does not block redirect-to-burn (blacklisted) addresses. The reward payout uses super._update which is the base ERC-20 transfer and bypasses the override. A blacklisted wallet therefore receives the reward in its balance but cannot transfer it without triggering a burn through the override.
Acknowledged
#14 informational Issue
Self-Tax Gas Cost on User Sells Above Swap Threshold
The override _update calls _collectTaxes which can call _swapAndDistribute when the combined autoLp and marketing buffers exceed SWAP_THRESHOLD. The full cycle - swap MHT to BNB, addLiquidityETH, marketing transfer - runs inside the user's transaction context and consumes their gas. This is industry standard for tax tokens.
Acknowledged
#15 informational Issue
Single-Step transferOwnership Regression
transferOwnership executes immediately without an acceptance step from the new owner. If the owner accidentally passes a wrong address (typo, copy-paste error), ownership is irrevocably lost and all admin functions become permanently inaccessible.
Resolved
#16 informational Issue
Dead Code _swapping Toggle Around super._update in rescueTokens
rescueTokens wraps super._update with _swapping = true / _swapping = false. Because super._update calls the base ERC-20 _update, the override gating on _swapping is never reached. The toggle is dead code that mis-leads readers about the function's behaviour. Identical in shape to the previously-resolved MHT-I-15 finding for the constructor.
Resolved
#17 informational Issue
No Event Emitted for BNB Forwarded to Marketing Wallet
After the M-04 fix, three paths can move native BNB from the contract to marketingWallet. None emitted a dedicated event in V2.2_corrected. The aggregated SwapAndDistribute event covered only the _swapAndDistribute path. Monitoring tools that wanted to distinguish sources or detect silent forwarding failures could not do so from on-chain logs alone.
Resolved
#18 informational Issue
nonReentrant Should Be the First Modifier on rescueTokens
rescueTokens declared modifiers in the order onlyOwner nonReentrant. Static analysers (Aderyn, Slither) recommend that nonReentrant always be the first modifier so any future modifier added to the chain is automatically covered by the re-entrancy guard.