PlotDex Info
Invest in tokenized fractional real estate from $25. Power the world's first pricing standard for property. Build on the blockchain designed for real estate.
Team and KYC Verification
The KYC verification for this project is currently in progress.
The team has submitted their information and verification is pending.
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 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 PresaleCore contract implements a 44-stage presale for the PLOD token on Binance Smart Chain, accepting BNB and a whitelist of registered ERC-20 payment tokens. Pricing is done through Chainlink V3 feeds with strict per-feed sanity bounds and a per-feed staleness window. The overall design follows established presale patterns with several thoughtful defensive additions: every Chainlink feed change is gated by a 48-hour timelock, the treasury rotation has a separate 48-hour timelock, fee-on-transfer payment tokens are rejected by a post-balance check on every buy, and unregistered feeds fail closed rather than open. The 44-stage allocation table is hand-typed but verified at deploy time by a defence-in-depth assertion that the per-stage allocations sum to TOKEN_CAP. The following areas need attention:
- The owner has wide discretion to reshape the active stage timeline. They can shorten the current stage to within fifteen minutes of now, can skip a stage entirely (with the explicit caveat in the source that unsold tokens in a skipped stage are permanently forfeited), can re-schedule individual stage starts, and can end the sale at any time without a timelock. The optional slippage parameter on every buy mitigates the worst case but only when the frontend actually populates it; a buyer who passes zero gets no protection.
- There is no automatic or permissionless way to end the sale. If demand falls short of the full cap and the owner is unresponsive, the sale stays in the started-but-not-ended state. Because the vesting vault gates the TGE timestamp on the presale having ended, this state would also block every buyer, advisor and team claim on the vault side. Pairing the existing manual endSale with an automatic flip when the token cap is reached, or with a permissionless fallback after the last stage's nominal end, removes this deadlock.
- Stage timing setters check that the sale has started but do not check whether it has ended. After sale closure no buyer outcome can change, but the owner can still mutate historical stage data. Adding the missing saleEnded guard keeps post-sale records immutable for explorers and indexers.
- Both contracts in the presale flow inherit single-step Ownable. A mistyped transferOwnership leaves the project without admin access because renounceOwnership is deliberately disabled. Switching to Ownable2Step requires the receiver to call acceptOwnership and eliminates this fat-finger risk.
- The presale pause has no maximum duration. Because buyer funds are always forwarded atomically to the treasury within the same transaction, a pause cannot trap money already in flight, but it can still indefinitely deny service to future buyers. A maximum pause window or a strict emergency-only policy publicly documented would close the gap.
Ownership Privileges
The ownership of the contract has been retained by the deployer and is intended to be a multisig. The owner retains full privileges including:
- Starting and ending the sale, pausing and unpausing buys, and manually advancing or skipping stages.
- Editing stage durations and start timestamps for the current and any future stage, bounded by a fifteen-minute floor.
- Toggling auto-advance on sellout without a timelock.
- Scheduling and executing treasury changes through a 48-hour timelock with a cancel path.
- No ability to mint or move PLOD; the contract does not custody PLOD and never holds it.
- No ability to take buyer funds for itself: every successful buy forwards the full payment to the treasury within the same transaction.
- No ability to bypass the price-sanity bands or the staleness window without going through the 48-hour timelock; immediate edits are subset-only and can only tighten the existing values.
- RenounceOwnership is deliberately disabled, so admin cannot be lost accidentally during the sale.
Security Features
The contract implements several positive security features:
- Forty-eight-hour timelock on every change that introduces or rotates a Chainlink feed and on every treasury rotation, with cancel paths and full re-validation of feed decimals at execute time.
- Per-feed minimum and maximum acceptable price bounds plus a per-feed staleness window, both bounded by a global floor and ceiling, with a tighten-only immediate path and a widen-only timelocked path. This defends against the LUNA-style collapse pattern where a feed is clamped to a low rail, against stable-coin depegs, and against a compromised owner pointing the contract at a malicious aggregator.
- Fee-on-transfer payment tokens are rejected on every buy by comparing the treasury's balance before and after the safeTransferFrom call.
- Checks-effects-interactions ordering throughout the buy paths, with nonReentrant on every value-moving external entry point.
- Slippage protection via a buyer-supplied minTokensOut parameter on both BNB and ERC-20 buys, evaluated before any state mutation.
- A constructor-time defence-in-depth assertion that the hand-typed per-stage allocations sum exactly to TOKEN_CAP, which catches future table edits that drift away from the headline cap before deployment can succeed.
Note - This audit report consists of a security analysis of the PresaleCore smart contract. This analysis did not include economic analysis of the contract's tokenomics. Moreover, we only audited the main contract for the PlotDex team. Other contracts associated with the project (the token and the vesting vault) were audited in parallel but reported separately, and the broader PlotDex ecosystem was not part of this engagement. We recommend investors do their own research before investing.
Files and details
Findings and Audit result
medium Issues | 1 findings
Pending
#1 medium Issue
Owner can shorten the active stage to 15 minutes or skip it without notice
The owner can shorten the duration of the currently-active presale stage down to roughly fifteen minutes from now via updateStageDuration, can skip the stage entirely with advanceStage (the unsold portion is permanently forfeited, which the team has documented in the code), can re-schedule individual stage starts with setStageStartTimestamp, and can permanently end the sale with endSale. None of these paths require a timelock. A buyer who has approved tokens and is about to call buyWithToken can see their effective stage shift to the next, higher-priced stage. The optional minTokensOut slippage parameter protects users who set it; users who pass zero get no protection.
low Issues | 5 findings
Pending
#1 low Issue
Single-step ownership transfer on the presale admin
PresaleCore uses the single-step OpenZeppelin Ownable. If the current owner calls transferOwnership with a mistyped address, the presale's full admin surface moves to that address in one transaction with no recovery path. The contract correctly disables renounceOwnership, so the only way out of a bad transfer is the new owner manually returning ownership - which requires that the wrong address is actually controlled and willing to cooperate.
Pending
#2 low Issue
Sale pause has no maximum duration
PresaleCore inherits OpenZeppelin's Pausable and exposes onlyOwner pause and unpause entry points. There is no maximum pause duration. A malicious or compromised owner can therefore stop all buys indefinitely. The buyer-funds-not-held-in-contract design keeps this from being a fund-loss issue, but it can still be used to grief participation in the presale.
Pending
#3 low Issue
Treasury must accept BNB but the contract never probes it
Each successful BNB purchase forwards msg.value to the treasury through a low-level call and reverts if the call fails. If the treasury is a contract whose receive function reverts, runs out of gas during the forward, or refuses BNB once a balance cap is reached, every buyWithBNB transaction reverts. The treasury change flow does not test the new address before committing, so a misconfigured treasury can silently break all BNB buys for the duration of the 48-hour timelock window plus the time it takes to detect and fix.
Pending
#4 low Issue
Floating pragma and inconsistent compiler version across the project
PresaleCore and VestingVault use pragma solidity ^0.8.34 while PLOD uses ^0.8.24. The caret accepts any version greater than or equal to the floor within the 0.8.x range, which means a later rebuild can produce code that differs from the audited bytecode. Pinning the pragma and harmonising it across the project keeps deployment reproducible.
Pending
#5 low Issue
Stage timing remains editable after the sale has ended
The three stage-timing setters - updateStageDuration, setStageStartTimestamp and batchUpdateStageDurations - all check if (!saleStarted) revert NotStarted but none of them check saleEnded. Once the sale has ended (either via endSale or by selling out the last stage) the owner can still mutate stage durations and start timestamps. Because every buy path reverts with AlreadyEnded after sale end, no buyer-facing outcome can change. The concern is history-integrity: explorers and indexers reading post-sale stage state will see records the owner can still modify, which complicates audits of the sale's actual timeline after the fact.
optimization Issues | 10 findings
Pending
#1 optimization Issue
StageConfig could be packed to reduce per-buy SLOAD cost
Each StageConfig occupies three storage slots because the three uint256 fields cannot share one. The table is 44 entries, which means 132 slots are written in the constructor and one SLOAD per field is paid per buy. The actual ranges of price, tokenAllocation and duration each fit comfortably into smaller types, so a packed layout reduces storage from three slots to one per stage and saves both deploy gas and per-buy gas.
Pending
#2 optimization Issue
Replace storage += with read-add-write via a stack-cached local
Several state variables are updated with the compound assignment operator. For storage variables, the equivalent x = x + y form using a stack-cached local saves a small amount of gas because the compiler generates a single SLOAD plus a single SSTORE instead of the load-modify-store pattern with extra arithmetic stack juggling. Affected lines include the per-buy totals updates and the beneficiary allocation tracking.
Pending
#3 optimization Issue
Loop counter increments can use unchecked
Each for loop uses a counter that cannot realistically overflow given the bounded indices. Solidity 0.8 inserts a checked increment by default. Wrapping the counter increment in an unchecked block removes the check and saves a small amount of gas per iteration. The savings add up over the constructor's 44-stage initialisation loop.
Pending
#4 optimization Issue
Cache array lengths outside the loop header
Reading the length of an array inside the loop condition costs one MLOAD per iteration. Caching the length into a stack local before the loop is a standard micro-optimisation and applies in both the constructor and the batch stage-update path.
Pending
#5 optimization Issue
Constructor can be payable
A non-payable constructor has an implicit msg.value == 0 check inserted by the compiler. Marking the constructor payable removes that check and saves a small amount of deployment gas. There is no functional effect at runtime.
Pending
#6 optimization Issue
Nested if can be cheaper than the boolean and-and
Short-circuiting with the boolean and-and operator inside an if generates slightly more bytecode than splitting into two nested ifs. The two forms are semantically equivalent given short-circuit semantics, and the nested form is preferred when both conditions test the same kind of fail-fast guard.
Pending
#7 optimization Issue
Cache currentStageIndex and stages[purchaseStage].price in _processPurchase
_processPurchase reads stages[purchaseStage] multiple times for tokenAllocation, price, and duration. Cache the StageConfig storage pointer or the specific fields into stack locals so each is loaded once. Same applies to stageStates[purchaseStage].
Pending
#8 optimization Issue
getAllStages returns a large memory payload
Returning two fixed-size arrays of 44 structs allocates a large memory block and is more expensive than necessary for any caller that only needs a single stage. The function is useful for off-chain bulk reads but should be paired with documentation steering on-chain consumers towards getStageInfo for single-stage lookups.
Pending
#9 optimization Issue
_recalculateTimestamps loop pattern can be tightened
The recalculation loop reads the previous stage's startTimestamp and duration anew on each iteration. Caching the running cumulative timestamp into a stack variable across iterations removes one storage read per iteration. The saving compounds across 44 iterations in the constructor and on every cascade update.
Pending
#10 optimization Issue
Replace remappings for OpenZeppelin imports with named imports where possible
Several imports use the plain form that pulls in everything from the imported file. Forge formatter recommends named imports that list the specific symbols used. This keeps the dependency surface explicit and helps reviewers see at a glance which symbols are pulled in from each library.
informational Issues | 5 findings
Pending
#1 informational Issue
BNB transferred to the contract is not address-checked but the design intentionally allows anyone to buy
Static analysis flags buyWithBNB because it forwards msg.value without checking msg.sender against an allow-list. For a public presale, the lack of a sender check is the intended behaviour: anyone is allowed to send BNB to acquire PLOD. The contract still validates msg.value > 0, the per-feed price band, the stage cap and the hard cap, so the unbounded sender is bounded by the rest of the buy preconditions.
Pending
#2 informational Issue
Abbreviations BNB, USD, TGE are kept capitalised in function names against the Solidity style guide
Functions such as buyWithBNB, setTGETimestamp and constants such as MIN_BUY_USD keep their domain-specific abbreviations capitalised. The Solidity style guide prefers strict mixedCase. The project is consistent across both contracts but the linter notes the deviation. This is purely stylistic.
Pending
#3 informational Issue
Stage prices and allocations are 109 hardcoded literals
The 44-stage pricing schedule is written using numeric literals for both price and allocation. The static analyser would prefer a named constant for each value. The team has explicitly added a runtime assertion that the sum of allocations equals TOKEN_CAP at deploy time, which is the more useful safeguard. Keeping the literals inline keeps the table compact and easy to compare against the whitepaper's stage list.
Pending
#4 informational Issue
External call inside the constructor loop validates feed and token decimals
The constructor calls decimals() on each registered feed and on each registered payment token. These external calls happen inside a for loop. In post-deployment functions this pattern is a denial-of-service risk because an attacker can pass a malicious target that consumes gas. In a constructor the inputs come from the deployer and the loop runs once, so the pattern is acceptable here.
Pending
#5 informational Issue
rescueBNB and rescueTokens are admin escape hatches with no normal in-flight funds to mishandle
rescueBNB and rescueTokens are typically dangerous because they can withdraw any balance held by the contract. In PresaleCore's design these functions only see funds that were force-sent to the contract by external parties, because every successful buy forwards payment to the treasury within the same transaction. The team documented this in the source. We mention it here so reviewers know to read the docstring before raising the rescue flag.