Plexrum Info
Universal Ownership for real-world assets. Plexrum is a unified ecosystem where a property stays scarce — but its ownership becomes infinite. Access · Exchange · Create · Unlock — all on-chain.
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 twenty-one-stage presale for the PLXR token on Ethereum, accepting the native currency and registered ERC-20 payment tokens priced through Chainlink feeds. It enforces a three-hundred-million token cap, a forty-nine-million dollar hard cap, a ten-dollar minimum purchase, and per-buyer loyalty tier tracking. Purchases are all-or-nothing against the remaining stage allocation, with no partial fills. This is the second remediation round on this contract and the work in it is of high quality. No High or Medium severity issue remains open. The blocking oracle finding from the previous engagement is properly closed and was re-verified against live mainnet round data on four aggregators. The permissionless end-of-sale path is now genuinely outside the owner's control, which also closes the vesting-side claim deadlock that had been open since the first engagement. The treasury acceptance probe was reordered correctly and the high-confidence reentrancy finding the static analyser previously raised against it is gone. A hand-check confirms the stage table still reconciles exactly to both headline caps: the per-stage allocations sum to three hundred million tokens and price out to exactly forty-nine million dollars. What remains are low-severity items, most of them residuals of the fixes themselves, and they share a pattern worth stating plainly: each fix is correct in the place it was applied and incomplete one step away from it. The areas that need attention are these:
- The constant that anchors the new finalization deadline no longer matches the stage schedule it was calibrated against. The constant is eight hundred and fifty-five days and its comment itemises that figure as three stages of twenty-one days plus eighteen of forty-four, which describes the previous revision's table. This revision changed the first three stages to forty-four days, making the real schedule nine hundred and twenty-four days, and the constant was not updated. The intended ninety-day grace window after the final stage is therefore twenty-one days in practice. More consequentially, because the schedule already overruns the constant, extending it by a further twenty-one days in total, well inside the three-hundred-and-sixty-five-day per-stage limit the contract itself permits, moves the deadline to before the sale's own end, at which point any caller can flip the terminal sale flag while stages are still live and selling. The flag is one-way, so the remaining allocation becomes permanently unsellable. Deriving the constant by summing the configured durations in the constructor, the same way the allocation table is already self-checked against the token cap, removes the possibility of the two drifting apart again.
- One of the three stage-timing setters was missed by the unchecked-cast remediation. The two duration setters are now properly bounded to a realistic maximum, and the running timestamp chain accumulates at full width with an explicit fit check and a monotonicity assertion, all of which was verified across all twenty-one stages at the maximum accepted duration. The start-timestamp setter received none of that treatment and still narrows an unbounded caller-supplied value into the sixty-four-bit storage field, with every guard above that write passing for a value at the type boundary. The cast truncates, the stored start can land on zero, and the recalculation then chains every later stage off that zero without reverting. The consequence goes beyond wrong dates: every expiry check in the contract is guarded on the start timestamp being greater than zero, so a stage dated zero never expires at all, while the stages after it become instantly unbuyable.
- The aggregator-reuse fix introduced an irreversible lockout. The new guard rejects a registration whose target aggregator already carries a price band, which correctly stops one token silently overwriting another's settings. Token removal, however, clears only the supported flag and leaves the feed pointer, decimals, band and staleness window behind, with no function able to clear them. Re-registering a removed token therefore hits the guard and reverts even when the token and the feed are both the originals, and every alternative route is closed: the rotation path requires the token to still be supported, and the immediate setters are tighten-only and reject a zero lower bound. The same applies to the native-currency feed, where an aggregator that has been rotated away from can never be rotated back to. Since there is normally one canonical aggregator per pair, a token removal is in practice irreversible and a feed rotation is one-way.
- The raised staleness ceiling is global, so the forty-eight-hour window the stablecoin feeds require is equally available to the native-currency feed, whose measured heartbeat is one hour. Widening does require the forty-eight-hour timelock, but re-pointing a feed at its own current address is an accepted rotation, so the window on the main purchase path can be stretched forty-eight-fold with no other change, after which a nearly two-day-old answer prices real purchases bounded only by the configured band. The same live sampling exposes the opposite error: a window set naively to the published one-hour heartbeat is tighter than the cadence actually observed, so purchases would revert intermittently. Each feed's window needs to be set from measured round data plus a buffer, and the native-currency feed would be better served by its own tighter ceiling.
- The stalled-stage finalization trigger added this round only covers the final stage. It works, and both the dust remainder and exact-sellout variants were verified finalizable by an arbitrary caller far short of the deadline. A dust-locked stage anywhere earlier is still unbuyable with no trigger available, and falls back on the anchored deadline roughly two and a half years out. The dust condition itself is stage-agnostic and already computed in the purchase path, so dropping the final-stage precondition closes this.
- Pause still has no maximum duration on this contract, and the resolution rests on the published operational policy the code cites three times by name. The remediation summary states that such a public document already exists. No copy of it was included with the sources and no published location was supplied, so this review could neither read it nor confirm that the code's citation resolves to something a buyer can find. The reasoning behind choosing a policy over a code bound here is sound and was re-confirmed: a pause blocks only future purchases, because every successful buy forwards payment to the treasury inside the same transaction, and an invariant held the contract's native balance at zero throughout. That is precisely why the vault was given a code bound and this contract was not, and the distinction is correctly drawn. What is needed is the link.
- Bytecode size is now the tightest constraint on this contract. It compiles to twenty-four thousand four hundred and ninety-three bytes against a limit of twenty-four thousand five hundred and seventy-six, so it fits with eighty-three bytes to spare, and only at one specific optimizer setting: at one thousand runs, an ordinary production value, the same source is over the limit. The build profile supplied with the sources pins neither the optimizer nor the run count nor the compiler version, so the configuration that produces a deployable artifact rests on convention. The automatic size check added this round is the right response to the finding and works correctly. It is worth pinning the build configuration and recovering some margin before the remaining items in this report are implemented, because eighty-three bytes does not accommodate them.
Ownership Privileges
The ownership of the contract has been retained by the deployer and is intended to be a multisig. Ownership transfer is two-step and renunciation is deliberately disabled. The owner retains full privileges including:
- Starting the sale once, which records the sale start timestamp permanently and sets every stage start by chaining the configured durations.
- Scheduling and then executing a stage advance or a sale closure, each subject to a one-hour public notice and each now lapsing if not executed within a four-hour window. Advancing may skip a stage that is neither sold out nor expired, in which case that stage's unsold allocation is permanently forfeited rather than rolled forward.
- Cancelling a scheduled advance or closure before it executes.
- Pausing and unpausing purchases, with no maximum duration.
- Scheduling, executing, and cancelling a treasury change, subject to a forty-eight-hour timelock and a value-bearing acceptance probe that now runs after state is committed.
- Scheduling, executing, and cancelling the registration of a new payment token or the rotation of any existing price feed, each subject to a forty-eight-hour timelock, with feed decimals re-validated at execution and the target aggregator checked against existing registrations.
- Removing a payment token immediately, which blocks new purchases with it but leaves existing buyer state untouched and, as noted above, cannot be undone.
- Tightening a registered feed's price band or staleness window immediately. Widening either requires the timelocked rotation path.
- Toggling auto-advance on sellout, with no timelock.
- Editing the duration or start timestamp of the current or any future stage, subject to a one-hour minimum remaining window, a three-hundred-and-sixty-five-day per-stage maximum on the duration setters, and rejection once the sale has ended. Strictly past stages are rejected, and the current stage's start can only be moved to the present, so this path cannot be used to shorten the window a buyer is relying on.
- Rescuing arbitrary ERC-20 tokens and stuck native currency to the owner. No normal code path leaves either in the contract.
- No ability to move the finalization deadline by any route, which is the substantive change in this round.
- No ability to mint PLXR, credit an allocation without payment, or alter a recorded purchase. The contract never holds or releases PLXR at all.
- No ability to redirect payment to anywhere other than the timelocked treasury address.
- Renunciation is deliberately disabled, so admin cannot be lost accidentally during the sale.
Security Features
The contract implements a substantial set of positive security features, several of which exceed what we would normally expect to recommend:
- Payment is never custodied. Every successful purchase forwards the full payment to the treasury within the same transaction, which an invariant confirmed by holding the contract's native balance at zero across one hundred and twenty-eight thousand fuzzed calls.
- A permissionless end-of-sale path anchored to a timestamp recorded once at launch plus two fixed constants, so no owner action of any kind can defer it and the sum cannot overflow. Verified against every stage-editing path individually and held as an invariant across the full fuzz campaign. This is what closes the claim deadlock on the vesting side.
- Per-feed staleness windows with both a floor and a ceiling, a fail-closed default for unregistered feeds, rejection of answers carried over from an earlier round, and rejection of uninitialised round metadata. The ceiling now clears the real cadence of every feed this deployment needs: across four aggregators and twenty-four intervals each, not one interval exceeded it, against eleven, thirteen, twenty-two and twenty-four of twenty-four that exceeded the previously shipped value.
- A forty-eight-hour timelock on every treasury change and on every path that introduces or repoints a Chainlink aggregator, including the registration of a brand-new payment token. A compromised owner key cannot atomically install a malicious feed and begin mispricing purchases.
- Per-feed minimum and maximum price bands that bound what any feed can quote, defending against a clamped feed, a depegged stablecoin, or a compromised aggregator. Bands can be tightened immediately but only widened through the timelock, and this subset-only property is enforced rather than documented. Registering an aggregator that another slot already uses is now rejected rather than silently overwriting it.
- Feed decimals validated to be exactly eight at registration and re-validated at execution time, so the hardcoded scaling in the USD math can never be silently violated. Payment token decimals are validated against the token's own on-chain value.
- Fee-on-transfer payment tokens are rejected by a post-transfer balance check on the recipient.
- Checks-effects-interactions ordering on both purchase paths and now on the treasury change as well, with state committed before any external call, plus reentrancy guards and pause gating on every value-moving entry point.
- Quote and purchase arithmetic agree exactly. Fuzzing across both payment paths confirms that a purchase with the slippage bound set strictly to the quoted value always succeeds and yields exactly the quoted token and dollar figures. A new maximum-payment view mirrors the purchase-path formula rather than inverting the quote, and was fuzzed across all twenty-one stages with partially filled allocations: the advertised maximum always executes and one unit above it never does.
- Owner-initiated stage advances and sale closures carry a one-hour public notice that now expires after four hours, so the notice is a real guarantee rather than an open-ended option.
- A defence-in-depth constructor check that the hand-typed stage allocations sum to the token cap, which catches a drifted stage table before the contract can be deployed.
- An automatic bytecode size gate in continuous integration, which is a useful addition on a contract this close to the limit.
- Twelve cross-contract conservation properties were fuzzed at one hundred and twenty-eight thousand calls each with no counter-example, covering both caps, per-stage reconciliation against the global counter, stage index bounds, immutability of the recorded sale start and of the finalization deadline, per-feed window bounds, and the bounds on every claim path.
- No High or Medium static-analysis finding beyond the previously triaged set. The High reentrancy finding on the treasury probe and both locked-ether findings from the previous revision are resolved, and the pattern scanner reports zero security-rule and zero best-practice-rule matches.
Note - This audit report consists of a security re-analysis of the PresaleCore smart contract following the second round of remediation of the previous engagement's findings. This analysis did not include economic analysis of the contract's tokenomics. Moreover, we only audited the main contract for the Plexrum team. The vesting vault was audited in parallel and reported separately. The PLXR token was not part of this submission, so the findings previously raised against it remain open and unverified. The broader Plexrum ecosystem including the presale frontend and off-chain treasury operations was not part of this engagement. The sources were supplied as archives without the dependency tree, so a build was reconstructed for analysis using the pinned compiler version and OpenZeppelin 5.6.1; re-verification against the team's own repository and a pinned build configuration is recommended before deployment. We recommend investors do their own research before investing.
Files and details
Findings and Audit result
high Issues | 1 findings
Resolved
#1 high Issue
Maximum feed staleness window was shorter than the real update interval of the Ethereum mainnet price feeds, so purchases reverted and stablecoins could not be registered
Previously reported. The ceiling on every per-feed freshness window was a compile-time constant of one hour. Live sampling of the aggregators this deployment requires showed that ceiling sat below what the feeds actually deliver: the primary stablecoin answer was over thirteen hours old when measured, with a twenty-four hour median update interval. Stablecoin payment tokens could therefore not be registered with a workable window and native-currency purchases reverted intermittently. Because the ceiling was a constant, the direct setter tighten-only and the timelocked rotation path re-validating against the same value, no configuration change could correct it after deployment.
low Issues | 9 findings
Resolved
#1 low Issue
Treasury change forwarded value to the not-yet-trusted queued address before committing state, violating checks-effects-interactions
Previously reported. The value-bearing acceptance probe ran before any state was committed and the function carried no reentrancy guard, so the callee executed with the change still pending and the outgoing treasury still active. No exploit was constructible, since every reachable state-mutating entry point was either owner-gated or individually guarded, but it was needless exposure on a path whose entire purpose is to call an address that is not yet trusted.
Pending
#2 low Issue
One of the three stage-timing setters still narrows an unbounded caller-supplied value into a sixty-four bit storage field with no check, so the timestamp chain can still be corrupted silently
Previously reported against both duration setters and the running timestamp chain. Most of it is now fixed, but the start-timestamp setter was missed and still writes a narrowing cast of a caller-supplied value with no upper bound. Every guard above that write passes for a value at or beyond the sixty-four bit maximum: the value is in the future, so the past-timestamp check passes, and it is after the previous stage's end, so the overlap check passes. The cast then truncates. Setting a stage's start to two to the power of sixty-four stores zero. The recalculation that follows chains off that zero, so every later stage is re-dated to the nineteen seventies. Neither the stored value nor the recalculation reverts, and the monotonicity check added to the recalculation does not fire, because a start of zero followed by one duration is still strictly increasing. The consequence goes beyond wrong dates: every expiry check in the contract is guarded on the start timestamp being greater than zero, so a stage dated zero never expires at all, on the buy path and in the sale-active view alike, while the stages after it are instantly expired and unbuyable. Demonstrated by two tests.
Pending
#3 low Issue
The stalled-stage finalization trigger only covers the final stage, so a dust-locked stage anywhere earlier still halts the sale with no trigger available
Previously reported for any stage. Purchases are all-or-nothing against the remaining stage allocation and truncation means a maximal purchase can leave a sliver behind. With auto-advance disabled, which is a single owner call carrying no timelock, that sliver is unbuyable, the stage has not expired by time, and no finalization trigger applies. The added trigger resolves this for the final stage only: it returns immediately unless the current stage is the last one. A sale dust-locked on the first stage is unbuyable, reports itself inactive, advertises a maximum purchase amount of zero, and still refuses finalization for the entire anchored deadline, which is roughly two and a half years out. Demonstrated by test.
Pending
#4 low Issue
The constant that anchors the finalization deadline no longer matches the stage schedule it was calibrated against, shrinking the grace window and letting a live sale be closed early
New, and introduced by the remediation itself. The finalization deadline is now the recorded sale start plus a fixed maximum total sale duration plus the grace window, which is the right shape and closes the previous deferral routes. The maximum total sale duration is eight hundred and fifty-five days, and its comment states that this matches the deployed schedule exactly, itemised as three stages of twenty-one days plus eighteen stages of forty-four days. That itemisation describes the previous revision's stage table. This revision changed the first three stages from twenty-one days to forty-four, so the schedule is now twenty-one uniform forty-four day stages, which is nine hundred and twenty-four days. The constant was not updated. Two consequences follow. The grace window after the final stage's scheduled end is intended to be ninety days and is in fact twenty-one days, because the deadline lands nine hundred and forty-five days after the start while the schedule ends at nine hundred and twenty-four. And because the schedule already overruns the constant, extending the schedule by a further twenty-one days in total, well within the three hundred and sixty-five day per-stage bound the contract itself permits, moves the deadline to before the sale's own end, at which point any caller can flip the terminal sale flag while stages are still live and selling. Demonstrated by test at one hundred days per stage: the deadline arrives around the halfway point of the schedule and an unrelated address closes the sale. The flag is one-way, so the remaining allocation becomes permanently unsellable.
Pending
#5 low Issue
Removing a payment token permanently burns its price feed, so neither the token nor a rotated-away aggregator can ever be registered again
New, and a side effect of the aggregator-reuse guard added this revision. The guard rejects a registration whose target aggregator already has a price band, because bands and staleness windows are keyed by aggregator address rather than by token. Token removal, however, only clears the supported flag; it leaves the price feed pointer, the declared decimals, the price band and the staleness window in place, and no function can clear them. Re-registering a removed token therefore hits the guard and reverts, even when the token and the feed are both the originals. Every alternative route is closed: the rotation path requires the token to still be supported, which removal undid, and the two immediate setters are tighten-only and reject a zero lower bound, so the band cannot be cleared. The same lockout applies to the native-currency feed: once rotated away from, an aggregator can never be rotated back to, because the exception the guard makes is only for the feed currently in use. Since there is normally one canonical aggregator per pair, the practical effect is that a token removal is irreversible and an ETH feed rotation is one-way. Demonstrated by two tests.
Pending
#6 low Issue
The raised staleness ceiling is global, so the forty-eight hour window the stablecoins need is equally available to the native-currency feed whose real heartbeat is one hour
New, and the direct consequence of the shape of the fix for the oracle finding. The ceiling had to be raised to accommodate the slowest feed the deployment needs, and it applies to every feed alike. Live sampling puts the native-currency heartbeat at one hour, a worst observed interval of three thousand six hundred and thirty-six seconds, against a ceiling that is now forty-eight times that. Nothing in the contract prevents configuring that feed with the slow feeds' window. Widening does require the forty-eight hour timelock, but re-pointing a feed at its own current address is an accepted rotation, so the window on the main purchase path can be stretched to the ceiling with no other change. Demonstrated by test: after such a rotation a native-currency answer nearly two days old prices both a quote and a real purchase, bounded only by the configured price band. With a band wide enough to cover ordinary volatility, a two-day-old answer is a large mispricing surface on the path that carries most of the volume. The same sampling exposes the opposite error: a window set naively to the published one-hour heartbeat is too tight for the cadence actually observed, so purchases would revert intermittently, also demonstrated by test.
Acknowledged
#7 low Issue
Presale pause still has no maximum duration, and the operational policy the code defers to could not be located for verification
Previously reported across both contracts. The vault half was resolved with a code bound and is reported separately. On the presale the position is unchanged in code: the pause has no expiry, and the resolution relies on the published operational policy the code cites three times by name. The remediation summary states that a public pause policy document covering this already exists. No copy of it was included with the sources and no published location was supplied, so this review could neither read it nor confirm that the code's citation resolves to anything a buyer can find.
Acknowledged
#8 low Issue
Sale pause has no maximum duration
Originally reported in the first engagement. The pause is intended as an emergency stop but carries no maximum duration, so a malicious or compromised owner can halt the sale indefinitely. Buyer funds are never held by the presale, so the harm is limited to blocking future participation.
Resolved
#9 low Issue
Floating pragma and inconsistent compiler version across the project
Originally reported in the first engagement, where the two larger contracts pinned a floating caret at one version while the token pinned a floating caret at an older one, so a future build could compile against an unaudited compiler. The previous engagement found the carets removed but the token pinned to a different exact version, leaving two exact pins with no overlap and no single compiler able to build the project.
optimization Issues | 1 findings
Pending
#1 optimization Issue
Gas and style scan results, none of which should be applied without checking against the fixes made this revision
New, and recorded as a caution rather than as a recommendation. The pattern scanner reports thirty-six findings across both contracts, all gas or style: state variables read inside loops, conditions that could be nested rather than combined, and postfix increments that could be prefix. None are security relevant. One of them is actively wrong to apply: the scanner flags the vault constructor as non-payable and suggests making it payable, which is precisely the change the previous engagement reported as an unrecoverable ether trap and which was correctly reverted this revision. The static analyser separately notes that the reentrancy guard is not the outermost modifier on the entry points that carry it, which is harmless here because the modifiers it follows are access checks.
informational Issues | 14 findings
Pending
#1 informational Issue
Presale runtime bytecode leaves eighty-three bytes of margin under the size limit, and the build profile that achieves it is not pinned
Previously reported as informational, and still informational. The presale compiles to twenty-four thousand four hundred and ninety-three bytes against the twenty-four thousand five hundred and seventy-six byte runtime limit, so it fits with eighty-three bytes to spare. That figure holds with the optimizer enabled at two hundred runs, which is the setting the previous engagement measured against and the one used here. It is sensitive to the setting: at one thousand runs, an ordinary production value, the same source is one thousand one hundred and ninety-nine bytes over the limit, and with the optimizer off it is well over. The build profile supplied with the sources declares no optimizer setting, run count, compiler patch level or target EVM version, so the configuration that produces a deployable artifact is established by convention rather than by the repository. Measured against the previous revision under identical settings, this round added one thousand eight hundred and fourteen bytes and consumed ninety-six percent of the margin that existed before it.
Resolved
#2 informational Issue
Per-feed price band and staleness window are keyed by aggregator address, so registering a second payment token on an existing feed rewrote the first token's band
Previously reported. Bands and windows are stored against the aggregator, not the token, so two tokens sharing a feed collapsed into one configuration and the later registration silently overwrote the earlier one's settings.
Resolved
#3 informational Issue
A scheduled stage advance never expired, so the notice window was a delay rather than a guarantee against a price shift
Previously reported. Once the one-hour notice had elapsed the owner could execute the advance at any later block, including the same block as a buyer's pending transaction, which defeated the purpose of the notice.
Resolved
#4 informational Issue
Quote functions clamped to the remaining stage allocation but the purchase path did not, and no view inverted the clamped figure back into a payment amount
Previously reported. A quote would helpfully clamp its output to the remaining headroom, but the purchase path rejects any amount whose derived token figure exceeds the stage allocation, and there was no way for a caller to discover the largest amount that would actually be accepted.
Resolved
#5 informational Issue
Struct packing documentation quoted the previous token cap, allocation, price and duration bounds instead of the current schedule
Previously reported. The packing invariants documented on the two stage structs were carried over from an earlier schedule version and no longer described the values actually deployed.
Resolved
#6 informational Issue
Permissionless finalization left a pending stage-advance schedule set in storage
Previously reported. Finalizing the sale through the permissionless path left any owner-scheduled advance or sale end sitting in storage, where both are meaningless once the sale has reached its terminal state.
Resolved
#7 informational Issue
Threshold lookup had no upper index bound and silently returned the top threshold for any out-of-range index
Previously reported. The threshold lookup fell through to the highest threshold for any index above the valid range instead of rejecting it.
Pending
#8 informational Issue
Purchase-on-behalf entry points let anyone assign a presale allocation to an arbitrary address without that address consenting
Previously reported and unchanged. Either purchase-on-behalf entry point credits an allocation to any address the caller names, with no opt-in from the recipient. The recipient's loyalty tier and aggregate spend move as a result, and the allocation becomes claimable only by that address. Where the named address cannot transact, for instance a contract with no ability to call the vault, the allocation is unclaimable and yet still counted as an outstanding vault obligation forever, because the presale's sold total never decreases and the vault's excess-rescue path is computed from it. That PLXR is not recoverable by anyone.
Pending
#9 informational Issue
Interface documentation drifted from the implementation in four places during this revision
New. The previous engagement raised stale documentation on the stage structs, which was fixed; the same class of drift then reappeared around the changes made this round. The staleness ceiling is documented as twenty-five hours while the constant is forty-eight. The finalization function is documented as having two objective triggers while the implementation has three, and the trigger it does describe is described with the old derivation, from the final stage's scheduled end, which is exactly the derivation the remediation replaced. The deadline reader repeats that old derivation verbatim. The grace window constant repeats it a third time. And the buyer tier reader documents a range of zero to eight while the tier library returns zero to nine. None of this affects behaviour, but the three restatements of the superseded deadline derivation are the kind of drift that makes a future reader reintroduce the finding that was just fixed.
Pending
#10 informational Issue
No maximum-payment view for the native currency, and an oversized hypothetical quote reverts rather than clamping
New, and the remaining half of the inverse-view gap. The token path now has an exact maximum-payment view. The native-currency path has none, so a caller holding ETH still cannot discover the largest amount the purchase path will accept. The quote function is not a substitute: it clamps its token output to the remaining allocation but does not return the payment amount that corresponds to the clamp, and probing it with a large hypothetical input reverts on arithmetic overflow rather than clamping, demonstrated by test. The code comment on the new view names this as the documented way to discover the actual maximum, which is accurate for tokens and has no counterpart for ETH.
Pending
#11 informational Issue
A lapsed schedule is never cleared, so the public slot keeps advertising a stage advance that can no longer happen
New, and a small residual of the execution-window fix. When a scheduled advance or sale end lapses past its execution window, the call reverts but the stored effective-at value is left in place. The reasoning given is that a revert would unwind a write anyway, which is true for the reverting call itself, but the slot is a public read that buyers, indexers and the frontend are explicitly invited to watch as an early warning. After a lapse it keeps reading as a live warning indefinitely. Demonstrated by test. The only paths that clear it are a successful execution, an explicit cancel, an auto-advance, or a permissionless finalization.
Pending
#12 informational Issue
The constructor performs feed registrations without the aggregator-reuse guard applied to every other registration path
New, and a gap in the coverage of the aggregator-reuse fix. The guard was added to the timelocked execution path but not to the constructor, which registers the initial payment tokens directly. Two tokens supplied with the same aggregator at deployment therefore still collapse into a single configuration, with the last one written winning. Demonstrated by test: a token registered with a tight band and a five-minute window silently ends up with the following token's loose band and forty-eight hour window. The same applies if a payment token is supplied with the aggregator already used for the native currency. The exposure is small, because construction is a single atomic step under the deployer's control and the outcome is visible in the emitted events, but this is a mispricing-adjacent misconfiguration that the contract now detects everywhere except at the one moment it is most likely to be made.
Pending
#13 informational Issue
The sale-start function narrows the running timestamp without the bound check applied to the equivalent recalculation
New as an observation, and the only remaining site the static analyser flags as an unchecked narrowing cast. The recalculation helper was given an explicit fit check before each narrowing cast, described in its own comment as defence in depth against a future edit changing the duration bound. The sale-start function chains the same sum across the same twenty-one stages into the same storage field and did not receive that check. It is unreachable today, because durations cannot be edited before the sale starts and the constructor values total nine hundred and twenty-four days, but the asymmetry means the stated defence-in-depth property does not actually hold for the project as a whole.
Pending
#14 informational Issue
The formatting gate in the new continuous integration workflow cannot pass, because no formatter configuration is declared for the width the code is written to
New, and narrow. A workflow was added this revision that checks formatting, builds with size reporting and runs the tests. The formatting gate fails on the sources as written: five of the six files contain lines longer than the formatter default width, and the build profile declares no formatter configuration to widen it, so the check wants to reformat code that is otherwise consistently styled. The presale source also carries carriage-return line endings while the other five files do not, which makes the formatter want to rewrite it in full; that difference may have been introduced when the sources were packaged rather than in the repository itself. Adding the workflow is the right move and the size gate in it is a genuinely useful addition, so it is worth the small effort to get it green.