EonX Info
EonX AI is the first-of-its-kind KYC-enabled AI Multi-Agent Blockchain, merging the intelligence of AI with the power of community. It is built to scale, built to last, and built by all. EonX AI marks the next evolution of blockchain, powered by the world’s first intelligent currency — iEX - the Smart Token. With KYC-enabled wallets, AI-regulated supply, and a Zero Dump – Zero Pump architecture, EonX AI sets a new standard for trust, stability, and innovation in decentralised ecosystems.
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.
Security Assessments
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 upgradeable
The contract uses a proxy pattern or similar mechanism, enabling future upgrades. This can introduce risks if the upgrade mechanism is not securely managed.
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.
Re-audit Update - IEXCBPV2.sol
This section records the re-audit of the new IEXCBPV2.sol against the original IEXCBP findings and the project team's audit-response report. The original analysis below describes the V1 contract and remains the baseline.
Two new findings were introduced in V2 and must be fixed before deployment
- Storage-layout incompatibility (Critical) - The V2 storage layout is incompatible with the audited contract. V2 drops OwnableUpgradeable from the inheritance chain (removing ~100 reserved base-contract slots under OZ v4) and removes the oracle storage slot, which shifts every subsequent variable including the users, directReferrals, rankCount and nicknameToAddress mappings. If V2 is deployed as an in-place upgrade of the existing proxy, all user balances and state are read from the wrong slots and are effectively destroyed. The layout must be validated against the actual deployed implementation (slither-check-upgradeability / OZ validateUpgrade) before any upgrade, and removed slots must be preserved as deprecated placeholders.
- Double-credited ROI in claimIncome (High) - claimIncome credits the ROI portion twice (once inside the if (roiPending > 0) branch and again in the roiPending + levelPending + uplinePending aggregate). This over-issues user-claimable balance up to the per-user cap, paying out ROI roughly twice as fast as the model budgets and breaking the totalEarnedUSD accounting. The aggregate must exclude roiPending.
Response-vs-code discrepancy
- The dev-key rotation is reported fixed but is not. The response states a devAddress state variable is now the active admin control address, but in the code devAddress is never assigned or read and the onlyDev modifier still authorises against the immutable DEV_ADDRESS constant. The dev role remains a hard-coded, non-rotatable EOA.
Smart Contract Analysis Statement
Contract Analysis
The IEXCBP contract is an upgradeable matrix / MLM compensation plan that funnels USDT through PancakeSwap into the IEX token, tracks 30 referral levels, 17 fund-reward tiers and 10 salary tiers, and pays withdrawals back in IEX based on a live PancakeSwap quote. The overall design follows familiar patterns on BSC, but several structural areas need attention before the contract can be considered production-ready:
- Many of the core configuration values (the level-percent, fund-reward and salary tables, the cooldowns, the maximum deposit, the controller address and the upline depths) are written as state-variable initializers at declaration. In an upgradeable contract these initializers run inside the implementation's constructor and never reach the proxy storage, so the live proxy boots with all of those values at zero. There is no on-chain setter for the level / fund / salary tables, which means the misconfiguration cannot be repaired without an upgrade.
- The withdrawal price comes from the live PancakeSwap spot rate. The user has no way to specify a minimum amount of IEX to receive, and the deviation oracle starts with a trusted price of zero, which makes the very first call to validateAndUpdate a no-op. The combination is exposed to ordinary sandwich attacks that lift value off withdrawing users.
- The migration setters can rewrite any user's claimable USD, investment list and rank counters with no event, no idempotence check, no length validation and no timelock. They are guarded only by a hard-coded developer address that cannot be rotated without an upgrade. From a power-of-the-key perspective they are equivalent to a mint authority over user-claimable USD.
- The COMPANY_ID account is exempt from the daily withdraw limit and from the per-investment cap multiplier that applies to every other user. The daily limit is the only structural circuit-breaker the protocol has against a single-key drain, and removing it for the most privileged account turns COMPANY_ID into the eject hatch.
- The upline-pool integration uses a single read of the qualified-downline count over the entire accrual window. When a sibling becomes qualified halfway through, the upline's pre-existing accrual is recomputed retroactively as if the new sibling had been there from the start, which can erase up to half of the legitimately earned amount on the next claim.
- The 30-day upline-qualification deadline and the 90-day ROI-booster deadline are one-shot windows with no admin extension and no event signal. Users who cross every numerical threshold one day late are permanently barred and have no on-chain way to learn why. The audit treats this as a Medium-severity behaviour pending confirmation against the product specification; the user-facing material should make the windows explicit either way.
- The "one action per block" guard is enforced inside deposit and withdraw but is not applied to the claim entry points, which means a user can deposit and chain several claim calls in the same block. This is either a deliberate carve-out that should be documented or an inconsistency that should be tightened.
- Several User-struct fields appear to be partially implemented (withdrawLockUntil is declared but never written, lastRoiWithdraw is migrated but never updated, salaryCycleTimestamp is set but never read, BASE_ROI is declared but never referenced). These fields should either be wired into a real path or removed; the present state increases the residual uncertainty around what behaviour is intended versus accidental.
Ownership Privileges
The ownership of the contract has been kept in two distinct roles - an OwnableUpgradeable owner that the team intends to set during initialize, and a hard-coded DEV_ADDRESS that is the actual operator of every administrative function. The dev role is not renounceable in any practical sense because it is baked into the code as a constant. The owner role can transfer ownership in a single step (no Ownable2Step). The dev key alone retains broad privileges, including:
- Lift the per-deposit ceiling (MAX_DEPOSIT_USD) to any value, change the daily withdrawal limit within a 500-3000 USD band, and change every cooldown.
- Re-point the price oracle and the controller-of-deductions address.
- Disable the price-deviation safety check (isSafe) and pause future deposits.
- Rewrite any user's totalDepositUSD, totalEarnedUSD, currentCapUSD, availableWalletUSD, freezeIncomeWallet (indirectly), investments list, direct referrals, level volumes, fund-receipt flags and rank counts through the migration setters.
- Withdrawals cannot be paused.
- There is no timelock on any administrative call.
- The dev key cannot be rotated on-chain; rotation requires a contract upgrade.
- The audit could not verify whether the dev or owner key is held by a multisig because the deployment context (proxy, owner, multisig, on-chain bytecode match) was not provided. The team must confirm this on-chain.
Security Features
The contract implements several positive security features:
- nonReentrant on every state-changing user entry point - deposit, withdraw, claimIncome, claimFundReward, claimSalary and claimRank1Reward.
- A per-user one-action-per-block guard via lastActionBlock that prevents same-block multi-call exploits.
- Slippage protection on deposit through the _minIexExpected parameter routed into swapExactTokensForTokensSupportingFeeOnTransferTokens.
- Per-deposit minimum and maximum amounts (100 and 3,000 USD), per-user daily withdrawal cap, per-referrer direct cap of 1,000, and a 24-hour first-withdraw delay after the first deposit.
Note - This Audit report consists of a security analysis of the IEXCBP smart contract. This analysis did not include economic analysis of the contract's tokenomics. The IEXCBP economic model is structured as a pay-as-you-go pool where every withdrawal is paid out of the IEX that previous deposits bought from PancakeSwap, which means solvency depends on continued inflows; investors should evaluate this independently. We only audited the IEXCBP contract for the iEX team. The IEX token and the IEXPriceOracle were used as references but were not audited. We recommend investors do their own research before investing.
Files and details
Findings and Audit result
critical Issues | 2 findings
Acknowledged
#1 critical Issue
Inline state-variable initializers are lost on a proxy deployment
The contract is written for the upgradeable pattern (it inherits Initializable, OwnableUpgradeable and ReentrancyGuardUpgradeable and reserves a __gap). A large group of state variables is given an inline initializer at declaration: CONTROLLER_ID, uplineDepth, uplineRewardDepth, isSafe, MAX_DEPOSIT_USD, DAILY_WITHDRAW_LIMIT, the three cooldowns and the level, salary and fund tables. In Solidity those initializers compile into the implicit constructor, which only writes to the implementation's own storage. When the contract is deployed behind a proxy, the proxy's storage is left at zero. The initialize function does not re-set any of these values and there is no setter for the level, fund or salary tables. The result on a live proxy is a fully misconfigured contract: deposits are rejected because MAX_DEPOSIT_USD is zero, level and fund income computations always produce zero, the deviation oracle is bypassed because isSafe is false and the 10 percent withdrawal deduction is sent to address(0) which the IEX token treats as a burn.
Pending
#2 critical Issue
Storage layout of V2 is incompatible with the audited contract; upgrading the existing proxy would corrupt all state
Relative to the audited IEXCBP.sol, IEXCBPV2 changes the storage layout in ways that are catastrophic for an in-place proxy upgrade. (a) It drops OwnableUpgradeable from the inheritance list. Under OpenZeppelin v4 this removes roughly 100 reserved gap slots contributed by OwnableUpgradeable and its ContextUpgradeable parent, which sit ahead of the contract's own variables; iexToken and every subsequent variable - including the users, directReferrals, rankCount and nicknameToAddress mappings - shift accordingly. (b) It removes the oracle storage variable, a full 32-byte slot at the original slot index 3, shifting CONTROLLER_ID and everything after it by one more slot. (migrationComplete and isSafe were packed bools and do not by themselves shift other variables.) (c) The V2 source imports ReentrancyGuardUpgradeable from the OpenZeppelin v5 utils/ path; if the client build has moved from v4 to v5 across versions, the base contracts switch from sequential gap slots to ERC-7201 namespaced storage, which is yet another different layout. If IEXCBPV2 is deployed as an upgrade to a proxy whose current implementation matches the audited layout, the mappings resolve from the wrong base slots and all user balances and state are read as zero or garbage - an irreversible state corruption. This must be reconciled against the actual deployed implementation before upgrading.
high Issues | 8 findings
Resolved
#1 high Issue
Migration setters can rewrite any user's claimable USD
Both migration helpers are guarded only by onlyDev (a hard-coded EOA-style address) and can write any field of the User struct, including availableWalletUSD, totalEarnedUSD, currentCapUSD, the investments array, the directReferrals array, the levelVolume array, the rankCount mapping and the fundsReceived flags. There is no length-mismatch check between _users, _isExists, _referrers and _nicknames in batchMigrateCore, and no length checks at all in batchMigrateArrays. batchMigrateArrays appends to investments rather than overwriting it, so re-running the migration with the same payload doubles every user's investments. No event is emitted and the migrationComplete flag declared at line 93 is never read or written, so there is no closed migration window. A compromised dev key can mint arbitrary USD-denominated income to any address and immediately drain the contract through normal withdraw calls.
Pending
#2 high Issue
Withdrawal pricing uses live PancakeSwap spot price with no slippage parameter
withdraw converts the user's USD-denominated payout to IEX with pancakeRouter.getAmountsOut(payoutUSD, [USDT, IEX]). There is no slippage parameter passed by the user. The deviation oracle, when isSafe is true, only verifies that the current spot is within maxDeviation (default 20 percent) of trustedPrice, and trustedPrice auto-rolls forward every 15 minutes. A searcher who sees a withdraw transaction in the mempool can sandwich it: buy IEX immediately before to push the IEX/USDT price up so the user receives fewer IEX, then sell after. Even a 5 percent sandwich extracts roughly 5 percent of the user's payout and stays well under the 20 percent oracle band.
Resolved
#3 high Issue
Oracle accepts any price while trustedPrice equals zero
The oracle constructor sets trustedPrice to zero and leaves the actual seeding to a separate owner-only initializeTrustedPrice call. The internal _withinDeviation function short-circuits to true when trusted is zero, which means every validateAndUpdate call returns true regardless of the live price until the owner remembers to seed the baseline. During that window the deviation guard does nothing and an attacker can manipulate IEX/USDT on PancakeSwap and drain the CBP at a manipulated rate.
Acknowledged
#4 high Issue
Fund rewards bypass the income cap by writing freezeIncomeWallet directly
When a user qualifies for a fund tier, _processFundReward writes user.freezeIncomeWallet += rewardNoCap directly without going through _creditIncome. The cap check, the cap-hit shutoff of dailyIncomeRate and the totalEarnedUSD bookkeeping are all skipped. Only tier 1 goes through _creditIncome. The fund-reward table is dominated by very large numbers, up to 5,000,000 USD for tier 17, so the bypass is significant and undermines the cap on which the rest of the solvency model depends.
Acknowledged
#5 high Issue
DEV_ADDRESS is a hard-coded constant and cannot be rotated
DEV_ADDRESS is declared as address public constant. It can only be replaced by deploying a new implementation, which itself requires the proxy admin role. Combined with the privilege footprint of the dev role (full migration writes, oracle re-pointing, controller re-pointing, MAX_DEPOSIT_USD lift, isSafe disable), a leaked or compromised dev key is a single-step path to draining the contract. There is no on-chain key rotation and no off-chain mitigation that can keep the role contained.
Acknowledged
#6 high Issue
Upline pool income discards or duplicates rewards on shareCount changes
_calculatePendingUplineIncome reads shareCount = _getQualifiedDownlineCount(uplines[i]) exactly once and applies that ratio across the entire timeDiff between the user's last upline withdraw and now. shareCount can change at any moment because every successful _attemptUplineQualification flips a downline's isUplineQualified flag and changes the divisor. When a sibling becomes upline-qualified halfway through the window, the upline's pending balance is recomputed retroactively as if there had been two qualified siblings from the start, which can halve the legitimate accrual. Conversely, if a downline drops off, the remaining recipients are over-credited.
Acknowledged
#7 high Issue
COMPANY_ID bypasses the daily withdraw limit
withdraw wraps the entire daily-cap block in if (!isCompany), so COMPANY_ID is exempt from the DAILY_WITHDRAW_LIMIT (default 3,000 USD per day) that applies to every other user. The daily limit is the protocol's only structural circuit-breaker against a single-key compromise. Combined with the migration setters that can populate availableWalletUSD for any address and the per-investment 20,000x cap multiplier reserved for COMPANY_ID elsewhere in the contract, COMPANY_ID becomes the eject hatch: a compromised dev key plus a single COMPANY_ID withdrawal can clear the contract's IEX balance in one block.
Pending
#8 high Issue
claimIncome credits ROI income twice (over-issuance of claimable balance)
In claimIncome, roiPending is credited through _creditIncome inside the if (roiPending > 0) branch (uint256 credited = _creditIncome(user, roiPending); user.earnedFromRoi += credited;), and then credited a second time as part of the aggregate uint256 total = roiPending + levelPending + uplinePending; _creditIncome(user, total);. The audited V1 credited only once via the aggregate (it did not call _creditIncome inside the roiPending branch). As written, every claimIncome inflates freezeIncomeWallet and totalEarnedUSD by an extra roiPending. The double credit is bounded by currentCapUSD because _creditIncome clamps to the cap, but because freezeIncomeWallet flows to availableWalletUSD and then to withdraw() as real IEX, this is a direct over-payout of user-claimable value: a user reaches the per-user cap on the ROI component in roughly half the intended time, which accelerates drain of the pay-as-you-go pool and breaks the totalEarnedUSD versus sum(inv.earned) accounting invariant. Level and upline income are not doubled.
medium Issues | 7 findings
Acknowledged
#1 medium Issue
Unbounded upline traversal can hit the block gas limit
deposit calls _updateUplineVolumeAndStats which walks up to uplineDepth (default 300) referrers. For every upline it calls _recalculateDailyRate which loops the 30 referral levels and reads several arrays from storage. The product is up to 9,000 storage operations per deposit. _updateUplineRankCounts walks uplineRewardDepth (also 300) and writes to rankCount for every fund tier achieved (up to 17 in one transaction). Once chains reach the configured depth, deposits and fund-reward claims will start to fail with out-of-gas as the network grows.
Acknowledged
#2 medium Issue
Fund and salary qualification loops scale with directs and can DoS large accounts
MAX_DIRECTS is 1000 and several read paths scan every entry in directReferrals[user] twice. _processFundReward calls getAchieverCountPerLeg seventeen times in a single transaction; that helper does two passes over the directs array. With 1000 directs the worst case is roughly 34,000 iterations plus storage reads for each users[d], in addition to 17 * 300 SSTOREs in _updateUplineRankCounts. Users who hit the directs cap can no longer claim their fund or salary income because the transaction runs out of gas.
Resolved
#3 medium Issue
Withdraw cannot be paused; no emergency stop on the exit path
Only deposits can be paused via pauseFutureDeposit. If the price oracle, the IEX token or PancakeSwap is found compromised mid-incident, there is no on-chain switch to halt outflows. Operationally this is a containment gap: the contract cannot be put into a read-only state without an upgrade.
Acknowledged
#4 medium Issue
Per-user withdrawal protected by a contract-wide lastWithdrawTime
lastWithdrawTime is a single global variable. After any user withdraws, every other user must wait SWAP_COOLDOWN (default 3 seconds) before they can withdraw. A griefer who withdraws frequently keeps the global timer fresh and can slow every other user's withdrawal. With the default 3 seconds the impact is mostly cosmetic, but the dev can set SWAP_COOLDOWN to any value through updateConfig and turn this into a real DoS.
Acknowledged
#5 medium Issue
lastSalaryWithdraw only advances when pending salary is actually paid
Inside the salary cycle branch of processSalaryRank, the assignment user.lastSalaryWithdraw = block.timestamp lives inside the inner if (pending > 0) block. When monthsPassed is greater than zero but validMonths ends up zero (because the user has not grown salarySnapshotLegBC past the next stepVolume threshold), pending stays zero and lastSalaryWithdraw is not advanced. On every subsequent call the same earlier months are re-evaluated, and the first time the user finally crosses the threshold the loop pays out a back-pay that the contract did not budget for, because qualification is checked against the current currentLegBC rather than against the historical month-boundary value. The cumulative effect is non-deterministic and depends on how the user's network grows over time.
Resolved
#6 medium Issue
Implementation has no constructor and initialize is unprotected
The contract has no explicit constructor, which means it does not call _disableInitializers(). If the implementation is deployed and not initialised in the same transaction as the proxy, anyone can call initialize() directly on the implementation, become its owner and set up state that a future proxy upgrade may inherit. Even on the proxy itself, deployment and initialise must be atomic to prevent a front-runner from claiming ownership.
Acknowledged
#7 medium Issue
30-day and 90-day qualification windows lock users out forever
_attemptUplineQualification early-returns when block.timestamp is greater than user.activationTime + 30 days, and _checkBooster does the same with a 90-day window. A user whose downline grows past the qualification thresholds even one day after the window is permanently barred from upline-pool income (resp. ROI booster). There is no event, no setter to extend the window and no on-chain way to communicate the lockout to the user. Because the booster path only ever upgrades the ROI percentage, a permanently-locked user keeps the lower 0.7 percent baseline forever.
low Issues | 14 findings
Pending
#1 low Issue
Missing zero-address checks on initialize parameters
The initialize function writes _iexToken, _router and _oracle into storage without checking that they are non-zero. A misconfigured deployment would leave the contract pointed at zero and the first deposit would only fail later, deeper in the stack.
Resolved
#2 low Issue
Missing zero-address check on updateControllerAddress
Setting CONTROLLER_ID to address(0) is silently accepted. The IEX token treats transfers to address(0) as a burn, so the 10 percent withdrawal deduction would be burned every time a user withdrew rather than going to the controller treasury.
Resolved
#3 low Issue
Admin and migration functions emit no events
Off-chain monitoring, indexing and forensic review all rely on events. None of the admin-only state changes in this contract emit one. Combined with the wide privilege footprint of the dev role this is a meaningful operational blind spot.
Resolved
#4 low Issue
migrationComplete is declared but never read or written
The variable name implies an intent to lock the migration once a one-shot import has finished, but no code path reads or writes it. The migration setters can therefore be re-run forever, with no closing window.
Acknowledged
#5 low Issue
getIexAmountFromUsd is public but only called internally
The function is a thin wrapper around pancakeRouter.getAmountsOut and is intended for use inside withdraw. Exposing it as public can mislead integrators into reading it as an oracle price; it is not.
Acknowledged
#6 low Issue
Mutable storage variables follow the constant naming convention
These six storage variables are mutable but use the ALL_CAPS convention reserved for constants. Integrators reading the ABI may assume they cannot change.
Acknowledged
#7 low Issue
Ownable2StepUpgradeable is not used
Single-step ownership transfer means a typo on transferOwnership permanently severs the owner role. OZ Ownable2StepUpgradeable solves this by requiring the new owner to call acceptOwnership.
Resolved
#8 low Issue
dailyWithdrawnUSD reverts on underflow if DAILY_WITHDRAW_LIMIT is lowered mid-day
If a user has already withdrawn more than the new limit because the dev has just lowered DAILY_WITHDRAW_LIMIT, the next withdraw computes a negative subtraction in checked arithmetic and reverts with a confusing arithmetic underflow until the daily window resets.
Pending
#9 low Issue
earnedFromX counters can exceed actual credited amounts when the cap is hit
claimIncome adds the full pending amount to earnedFromRoi, earnedFromLevel and earnedFromUpline before calling _creditIncome, but _creditIncome may credit only a portion of that amount when the cap is reached. The aggregated earnedFromX values can drift higher than totalEarnedUSD, which confuses dashboards and downstream reporting even though no funds are lost.
Pending
#10 low Issue
MAX_INVESTMENTS is declared but never enforced
MAX_INVESTMENTS = 100 is declared as a constant but deposit never checks user.investments.length against it before the push. _calculatePendingROI and _distributeRoiToInvestments iterate the investment list every time the user is touched, so a user can grow the list large enough to gas-DoS their own claim path. Combined with the directs-loop gas-DoS it adds another category of accounts that can no longer claim.
Resolved
#11 low Issue
updateConfig allows _id == 9 to pass the require but always reverts in the else branch
The leading require(_id >= 1 && _id <= 9, "Invalid config id") allows _id == 9, but the if/else chain only handles 1-8 and the trailing else { revert("Invalid ID"); } always fires when _id == 9. This is dead code left over from a removed config slot. It is not directly exploitable but produces inconsistent error handling and a confusing public ABI.
Resolved
#12 low Issue
Divide-before-multiply causes precision loss in income and salary math
Three different code paths divide before they multiply. _distributeRoiToInvestments computes dailyIncome = (inv.amount * rate) / PERC_DIVIDER and only then multiplies by timeDiff, losing precision when inv.amount * rate is small. _calculatePendingUplineIncome divides the pool by PERC_DIVIDER squared before multiplying by timeDiff and dividing again by shareCount. processSalaryRank computes stepVolume = (target * 20) / 100 and only then multiplies by requiredMultiplier, which can let a user qualify earlier than the spec because the rounding-down direction goes their way. Cumulative effect is small but systematic and trivially fixed.
Resolved
#13 low Issue
approve return value ignored in _swapUsdtToIex
usdtToken.approve is called in _swapUsdtToIex without checking the return value. BSC USDT is BEP20-compliant and currently returns true, so the immediate behaviour is correct, but the pattern would silently fail against any USD-stable that does not return a bool, or against a future USDT upgrade that adds further checks.
Resolved
#14 low Issue
claimRank1Reward credits freezeIncomeWallet directly, bypassing the cap
The rank-1 fast-track pool credits users[msg.sender].freezeIncomeWallet += reward without going through _creditIncome. The reward is capped at 1 USD per day for 100 days so the absolute amount is small, but it bypasses the user's currentCapUSD and the cap-hit shutoff of dailyIncomeRate. The design intent should be made explicit either way.
optimization Issues | 8 findings
Resolved
#1 optimization Issue
Unused state variable migrationComplete
migrationComplete is declared at line 93 but never read or written. Static analyzers flag it as both unused and constable.
Resolved
#2 optimization Issue
Replace home-rolled _safeTransfer / _safeTransferFrom with SafeERC20
The contract rolls its own _safeTransfer / _safeTransferFrom using low-level calls. SafeERC20 is the established library and removes a hand-written low-level call from the audited surface.
Resolved
#3 optimization Issue
Use abi.encodeCall instead of abi.encodeWithSelector
abi.encodeWithSelector takes raw selectors and untyped arguments; abi.encodeCall validates the selector against the function signature and the argument types.
Acknowledged
#4 optimization Issue
Cache array length outside the loop header
Several loops repeatedly evaluate array.length in the loop header. Caching saves a few hundred gas per loop iteration on cold storage.
Acknowledged
#5 optimization Issue
Use prefix increment and unchecked counters where safe
Both the prefix-increment and the unchecked-increment patterns are well-known gas optimisations for loops.
Acknowledged
#6 optimization Issue
Replace require strings with custom errors
Custom errors are cheaper at deploy time and at runtime, and they encode richer information than a string.
Acknowledged
#7 optimization Issue
State-variable reads inside loops
Loop bodies repeatedly access storage variables that do not change within the loop. Each access is a cold or warm SLOAD that can be elided.
Acknowledged
#8 optimization Issue
Once-per-block guard duplicated across deposit and withdraw
require(user.lastActionBlock != block.number) and the matching write are duplicated across deposit and withdraw. A small helper cleans up the repetition.
informational Issues | 15 findings
Pending
#1 informational Issue
Floating pragma and outdated compiler
The contract uses a floating pragma ^0.8.19 and the upstream OpenZeppelin packages use ^0.8.0 / ^0.8.2. Compiling with a different patch version can change codegen subtly, especially around via-ir.
Pending
#2 informational Issue
Uses OpenZeppelin v4 upgradeable contracts; v5 is current
The security/ReentrancyGuardUpgradeable.sol import path is the v4 location. v5 has security improvements and is the actively-maintained line.
Acknowledged
#3 informational Issue
onlyDev modifier wraps a single check
forge fmt suggests wrapping the modifier body to a helper. Not security-relevant.
Acknowledged
#4 informational Issue
DEV_ADDRESS is hard-coded
DEV_ADDRESS is baked into bytecode. Even if rotated via upgrade, the cost and process are not obvious to the team.
Pending
#5 informational Issue
initialize emits no Initialized event
OwnableUpgradeable already emits OwnershipTransferred during __Ownable_init, but no event signals that the IEX token / router / oracle wiring is in place.
Acknowledged
#6 informational Issue
Numeric literals with too many digits
Several initializers use literals like 20000000 * 1e18. Static analyzers flag these as easy-to-misread.
Pending
#7 informational Issue
__gap visibility and sizing
uint256[50] private __gap is reserved at the tail. The size is currently fine for a contract of this state-variable count, but the team should remember to decrement it on every upgrade that introduces new state.
Resolved
#8 informational Issue
Misleading error message in withdraw
withdraw uses the string "Claim Cooldown" for what is actually a withdrawal cooldown driven by lastWithdrawTime + SWAP_COOLDOWN. The message is misleading for users.
Pending
#9 informational Issue
Bootstrap state for COMPANY_ID is not documented in NatSpec
COMPANY_ID receives substantially preferential treatment that is not visible from the spec. Documenting it explicitly avoids surprises during the next audit.
Acknowledged
#10 informational Issue
High cyclomatic complexity hampers reasoning and testing
Cyclomatic complexity of 13, 12 and 15 in three large functions makes both manual review and symbolic / fuzz testing harder. Splitting into helpers lowers the test surface.
Resolved
#11 informational Issue
Missing contract-level NatSpec
There is no @title / @notice / @author header. Future readers and auditors must reverse-engineer intent from the code.
Acknowledged
#12 informational Issue
Volume bookkeeping is asymmetric (added at depth 300, removed at depth 30)
_updateUplineVolumeAndStats adds the deposit amount to levelVolume[i] for i = 0..29 and to totalTeamVolume for every upline up to uplineDepth (300). _removeUplineLevelVolume only walks 30 entries when an investment matures and never decrements totalTeamVolume. As a result, the salary rank checks (which read totalTeamVolume through getLegVolumes) keep counting matured investments forever, while levelVolume is correctly reduced. Salary ranks can drift higher than the rest of the math is budgeted for.
Acknowledged
#13 informational Issue
Balance-diff IEX accounting is fragile under fee-on-transfer paths
IexBought = iexToken.balanceOf(this) - initialBal is correct only as long as no other transfer of IEX into this contract happens between the two reads. The nonReentrant modifier blocks same-contract reentry; cross-contract reentry through the IEX _update callback path is not currently possible because IEX has no external callbacks. The pattern is fragile in the sense that a future change in IEX or an additional token integration could invalidate the assumption.
Pending
#14 informational Issue
Dead or inactive control fields
Several User-struct fields are present in storage but only partially wired. withdrawLockUntil is declared but never read or written. lastRoiWithdraw is migrated by batchMigrateCore but never updated by any other path. salaryCycleTimestamp is set inside processSalaryRank but never read. BASE_ROI is declared as a public constant but never referenced - the actual baseline ROI is hard-coded as user.roiPercentage = 70 inside register. The combination suggests partially-implemented controls and increases the audit's residual uncertainty about which behaviour is intended versus accidental.
Pending
#15 informational Issue
One action per block guard is inconsistently enforced
lastActionBlock is checked and updated inside deposit and withdraw but is not touched by claimIncome, claimFundReward, claimSalary or claimRank1Reward. A user can therefore deposit and then chain several claim calls inside the same block, despite the function-level error message One action per block. This may be intentional but should either be enforced uniformly or stated as policy in the contract documentation.