ShopinX Info
The global retail industry is undergoing a significant transformation as customer acquisition and retention costs reach historic highs. Brands continue allocating billions of dollars to loyalty initiatives in an effort to retain customers within their ecosystems. However, a substantial portion of consumers ultimately abandon these programs due to the limited flexibility and utility of closed-loop reward structures. At the core of this challenge lies the traditional database architecture underpinning most loyalty systems, which prevents true user ownership and restricts the free movement of rewards and value across independent networks.
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 can blacklist addresses
It is 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.
Token transfer can be locked
Owner can lock user funds with owner functions.
Token can be burned
There is a function to burn tokens in the contract without any allowances.
Ownership is not renounced
The owner retains significant control, which could potentially be used to modify key contract parameters.
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 ShopinX Token (SPX) contract implements an ERC20 token with a fixed supply of 300,000,000 SPX, EIP-2612 gasless approvals, holder self-burn, owner-controlled time-locks, and an owner-controlled linear step-vesting module. While the overall design follows common patterns on Ethereum, a few areas need attention:
- The
setLockprimitive has no upper bound on the unlock timestamp and locks can only be extended, never reduced; the owner can therefore freeze any holder permanently and irreversibly, which is a de-facto blacklist regardless of how the project chooses to label it. transferWithLockstores its lock as a per-address timestamp rather than against a specific amount, so sending even a minimal transfer to an address applies the lock to that address's entire current and future balance.- The
availableToTransferview diverges from the rules enforced inside_updatewhenever an address has both a time-lock and a vesting schedule, which can mislead user interfaces and integrations about how many tokens are actually movable. - All administrative powers sit behind a single-key, one-step
Ownableowner, with no on-chain delay, no multisig requirement and no two-step transfer, andrenounceOwnershipis exposed and would permanently disable every administrative path if called by mistake.
Ownership Privileges
The ownership of the contract has been assigned to a single externally-owned account passed at deployment time through OpenZeppelin's Ownable, with no two-step transfer, timelock or multisig enforced in-contract. The owner retains full privileges including:
- Extending the time-lock on any address through
setLock, up to and includingtype(uint256).max, with no on-chain cap. - Combining a transfer with a time-lock on the recipient through
transferWithLock, which also freezes any unrelated balance the recipient already holds or later receives. - Creating a one-time vesting schedule on any address through
transferWithVesting, with freely chosen cliff, step duration and step percent. - Transferring or renouncing ownership in a single transaction.
- The owner cannot mint new tokens; the supply is fixed at 300,000,000 SPX minted once in the constructor and there is no
mintentrypoint or minter role. - The owner cannot set or change any fee or tax; no fee logic exists in the contract.
- The owner cannot upgrade the contract; the code is deployed directly, without a proxy, so the bytecode is immutable after deployment.
- The owner cannot reduce or remove an existing time-lock, cancel a vesting schedule, or amend a vesting schedule once it has been created, because the lock rule is strictly monotonic and
transferWithVestingrejects any target that already has a schedule.
Security Features
The contract implements several positive security features:
- It inherits from well-audited OpenZeppelin v5 building blocks (
ERC20,ERC20Burnable,ERC20Permit,Ownable) and compiles cleanly with Solidity 0.8.27, benefiting from built-in overflow and underflow checks on every arithmetic operation. - The supply is fixed at deployment and cannot be inflated; there is no mint function, no minter role and no fee or tax logic that could be tuned by the owner.
- The transfer gate is implemented in a single override of
_update, which means every token movement — transfers,transferFromand burns — goes through the same lock and vesting checks, avoiding the common class of bugs where an alternate code path bypasses access control. - The contract has no external calls, no low-level calls, no assembly, no delegate-calls and no upgrade mechanism, which eliminates reentrancy, storage collisions and proxy-upgrade risk by construction.
- EIP-2612 support is provided natively through
ERC20Permit, enabling gasless approvals for end users while relying on the battle-tested OpenZeppelin implementation of the permit signature logic.
Note - This Audit report consists of a security analysis of the ShopinX Token (SPX) smart contract. This analysis did not include economic analysis of the contract's tokenomics. Moreover, we only audited the main contract for the ShopinX Token (SPX) 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
Pending
#1 high Issue
Owner can irreversibly freeze any holder via setLock
setLock accepts any unlockTime strictly greater than block.timestamp, with no upper bound. Combined with the invariant that a lock can only ever be extended, never reduced, this allows the owner to call setLock(victim, type(uint256).max) and freeze every token the victim currently holds as well as every token they ever receive afterwards, because _update uses lockUntil[from] as the authoritative gate on every outgoing transfer. The freeze cannot be reduced or removed by any party afterwards, not even the owner, because of the monotonic-only rule.
Pending
#2 high Issue
transferWithLock freezes the recipient's entire pre-existing and future balance
transferWithLock stores the lock as a timestamp on the recipient's address (lockUntil[to] = unlockTime). Because _update checks that timestamp for every outgoing transfer, the lock extends to the recipient's whole balance at that moment and to any tokens they later receive from third parties, not just to the amount transferred in the call. A single wei of SPX sent to a market-maker, exchange hot wallet or retail holder with a far-future unlockTime freezes that address entirely for the duration of the lock.
medium Issues | 4 findings
Pending
#1 medium Issue
availableToTransfer view is inconsistent with the transfer rules enforced in _update
When an address carries both a time-lock (lockUntil in the future) and a vesting schedule, availableToTransfer evaluates only the vesting branch and can return a non-zero value, while _update still reverts the actual transfer because it checks block.timestamp >= lockUntil[from] first. User interfaces, routers, bots and accounting tools relying on this view will mislead users about what they can actually move. This also means transferWithVesting never inspects lockUntil[to] when writing a new schedule, which is a silent way to land an address in the inconsistent state above.
Pending
#2 medium Issue
Single-step Ownable with broad, irreversible operational powers
The contract uses a single-key, one-step Ownable for all administrative functions (lock creation, lock extension, vesting creation). A mistyped transferOwnership sends ownership to an unusable address in a single transaction, and there is no on-chain delay or co-signer requirement between a privileged call being initiated and it taking effect. Given the impact of setLock, transferWithLock and transferWithVesting, this access-control model is thinner than best practice for a production token.
Pending
#3 medium Issue
Vesting lien is enforced as a fungible reserve against the entire balance rather than a segregated escrow
The _update hook enforces balanceOf(from) >= value + (v.totalAmount - vestedAmount(from)). The result is a fungible reserve: any tokens in the account can satisfy the locked amount, and granted tokens are not distinguished from pre-existing or later-received tokens. This does not freeze unrelated balance, but it does couple the accounting of the grant to the beneficiary's wider balance, which can confuse off-chain tooling that expects vesting grants to be held in a separate escrow. It also means burning, slashing or clawing back granted tokens in isolation is impossible.
Pending
#4 medium Issue
Locked and undervested holders cannot burn their tokens
_update applies the lock and vesting checks to every non-mint transition, including _burn (which routes through _update with to == address(0)). A holder under an active lock or with an undervested grant therefore cannot exercise the burn / burnFrom interface inherited from ERC20Burnable. This contradicts the common user expectation that holders can always destroy tokens they own, and makes the advertised 'burnable' behaviour conditional in a way that is not documented.
low Issues | 3 findings
Pending
#1 low Issue
Floating Solidity pragma and default PUSH0 target
The contract uses a floating pragma ^0.8.27, allowing compilation with any compatible 0.8.27+ compiler. Combined with the default Shanghai target of modern solc, the emitted bytecode uses PUSH0, which is not universally supported across EVM-compatible chains. Two rebuilds of the same source can therefore produce different artifacts, and deployment can silently fail on chains that do not implement PUSH0.
Pending
#2 low Issue
Missing input validation on privileged functions
The privileged entrypoints do not validate inputs that have operational impact. setLock accepts address(0) because it never calls _transfer. transferWithLock and transferWithVesting do not require amount > 0, so a zero-amount call silently consumes a vesting slot forever (combined with the write-once rule on vesting). transferWithVesting also lacks upper bounds on cliffDays and stepDays, allowing pathological schedule parameters.
Pending
#3 low Issue
renounceOwnership is exposed and would be destructive
OpenZeppelin's Ownable exposes renounceOwnership, which sets the owner to the zero address. Given the design of this contract, a renounced ownership makes every existing lock permanent and every existing vesting schedule frozen in place, with no path to correct a mistake. There is no operational reason to keep this function active in a production deployment of this token.
optimization Issues | 4 findings
Pending
#1 optimization Issue
Mark public administrative and view functions as external
Several administrative and view functions are declared public but are never called from within the contract. public adds a small gas overhead because the arguments are copied from calldata into memory. external is the appropriate visibility whenever internal consumption is not needed, and it also communicates more clearly that these functions are part of the contract's external surface only.
Pending
#2 optimization Issue
Replace repeated numeric literals with named constants
The literal 100 is repeated in percent validation and in the vesting maths, and the initial supply 300_000_000 * 10 ** decimals() is written as an inline expression in the constructor. Promoting these to named constants improves readability and reduces bytecode duplication, while also making the cap part of the contract's public API.
Pending
#3 optimization Issue
Prefer multiplication before division to reduce precision loss
vestedAmount performs a division (afterCliff / stepSeconds) before multiplying by stepPercent, and computes currentStepAmount by dividing totalAmount by 100 before multiplying by currentStepElapsed. Reordering to divide last keeps precision maximal and prevents rounding errors from accumulating across steps.
Pending
#4 optimization Issue
Pack VestingInfo fields to reduce storage writes
VestingInfo declares five independent uint256 fields. The values stored there (token amounts bounded by the 300M supply, day counts and a percentage) all fit in smaller integer types, so the struct can be packed into fewer storage slots without loss of precision.
informational Issues | 8 findings
Pending
#1 informational Issue
Vesting schedules are write-once and cannot be cancelled or amended
The check require(vesting[to].totalAmount == 0) prevents any subsequent update to a vesting schedule once it has been created, and the contract exposes no function to cancel or amend one. Whether this is acceptable depends on the product requirements around vesting and is therefore flagged as a product and operations decision rather than a bug.
Pending
#2 informational Issue
Vesting releases tokens continuously within each step
After the cliff ends, vestedAmount releases tokens continuously each second inside the current step. Classic cliff-plus-step schedules typically unlock tokens discretely at step boundaries. Whether the continuous behaviour is correct depends on what the project has advertised; it is not itself a security bug, but it is a meaningful product decision worth explicitly documenting.
Pending
#3 informational Issue
permit allowances can still be signed while an account is locked
A locked holder can still produce valid EIP-2612 permit signatures because permit only updates allowance. A subsequent transferFrom will revert in _update because of the lock. This matches the standard semantics of permit, which never promises that a downstream transfer will succeed, and is therefore documented here as informational rather than as a vulnerability.
Pending
#4 informational Issue
Reliance on block.timestamp for time comparisons
All lock checks, vesting progress checks and availability checks use block.timestamp. Validators have a limited ability to influence this value, which is irrelevant at the day-level resolution used by the vesting and lock logic. Listed for completeness because it is an industry-standard disclosure.
Pending
#5 informational Issue
Modulo used for interpolation flagged as weak randomness
Static analysis reports afterCliff % stepSeconds as a weak pseudo-random number generator. The modulo here is purely a deterministic interpolation inside a vesting step and not a random value. Listed for documentation only.
Pending
#6 informational Issue
Strict equality v.totalAmount == 0 flagged as dangerous
Static analysis flags v.totalAmount == 0 as a dangerous strict equality. Here zero is the default value for an uninitialised VestingInfo, so the comparison is the standard way to check whether a schedule has been set. Listed for documentation only.
Pending
#7 informational Issue
Verify EIP-712 domain separator after deployment
ERC20Permit is initialised with the name 'ShopinX Token', which matches the ERC20 name. A routine post-deployment check should confirm that the domain separator computed off-chain matches the one returned by the deployed contract, to rule out metadata or constructor-argument drift.
Pending
#8 informational Issue
Recipient and initial owner addresses should be publicly documented
The constructor receives the initial 300M recipient and the administrative owner as two arguments, with no on-chain labelling of what each address represents. Clear documentation of these addresses is essential for downstream trust evaluation given the broad administrative powers described elsewhere in this report.