Every audit on SmartContract.us begins with a deterministic pre-scan — a layer of analysis that runs before the AI sees the contract source. Fifteen specific vulnerability patterns are checked using regex rules and structural heuristics. The results are injected directly into the Claude prompt so the AI can validate, refute, or expand on each finding in context.
This document covers all 15 patterns: what each one detects, why it is dangerous, what the fix looks like, and the severity rating assigned when the pattern matches. Understanding these patterns is useful whether you are evaluating the platform's coverage, hardening your own contracts, or learning what automated scanners look for.
How the pre-scan works
Pattern detection runs on the raw Solidity source code returned from the block explorer for the scanned address. Comments are stripped before reentrancy-guard heuristics run (to reduce false positives from commented-out code). Most patterns use regular expression matching with optional exclusion rules — for example, VPS-001 is not flagged if the code already wraps the .call in a checked return. Structural patterns like VPS-014 and VPS-015 use multi-pass logic that looks at the full function body, not just individual lines.
Findings from all 15 patterns are formatted and prepended to the Claude audit prompt. The AI is instructed to treat each finding as a hypothesis — confirming it with additional reasoning, refuting it with contract-specific context, or escalating it if the pattern is part of a larger vulnerability chain.
The 15 patterns
VPS-001 — Unchecked Low-Level Call (HIGH)
What it detects: Any .call{value:} expression where the boolean return value is not captured and checked.
Why it matters: The low-level call opcode never reverts on failure — it returns false. If the caller does not check this value, a failed ETH transfer is silently swallowed. The sending contract believes the transfer succeeded, creating a discrepancy between its internal accounting and the actual ETH balances. This pattern has caused direct fund loss in multiple production incidents.
Fix: (bool success, ) = addr.call{value: amount}(""); require(success, "Transfer failed");
Learn more about unchecked return values →
VPS-002 — tx.origin Authentication (HIGH)
What it detects: Any use of tx.origin in the contract source.
Why it matters: tx.origin returns the original initiator of a transaction, not the immediate caller. If a contract uses tx.origin for an ownership or authorization check, a malicious contract can trick an authorized user into calling it — and the malicious contract then passes the authorization check when it calls the target contract, even though the user never intended to authorize that action.
Fix: Replace tx.origin with msg.sender for all authorization checks. The only legitimate use of tx.origin is the check require(msg.sender == tx.origin), which ensures the caller is a wallet and not a contract.
Learn more about tx.origin vulnerabilities →
VPS-003 — Block Timestamp Dependence (MEDIUM)
What it detects: Any use of block.timestamp in the contract source.
Why it matters: Block proposers have a narrow but real ability to adjust the timestamp of the block they produce — typically within a 15-second range. For most uses (general logging, approximate cooldowns), this is harmless. For deadline enforcement, randomness, or game outcomes with significant economic value at stake, the manipulation window is large enough to be exploitable.
Fix: Avoid using block.timestamp for randomness (use Chainlink VRF instead) or for precise deadlines where a 15-second tolerance is unacceptable. For multi-day timeouts and cooldowns, the imprecision is generally acceptable.
Learn more about timestamp dependence →
VPS-004 — selfdestruct Usage (CRITICAL)
What it detects: Any call to selfdestruct() or the deprecated alias suicide().
Why it matters: selfdestruct permanently destroys the contract, sends all remaining ETH to a specified recipient, and critically bypasses the recipient's receive() and fallback() functions. This can be used to force ETH into contracts that are not designed to hold it, breaking invariants that rely on address(this).balance == 0. Additionally, selfdestruct is deprecated as of Solidity 0.8.18 and scheduled for removal from the EVM.
Fix: Remove selfdestruct and implement a withdrawal pattern instead — an access-controlled function that transfers remaining funds to a designated address. This is fully reversible (unlike a destroyed contract) and does not bypass recipient fallback logic.
Learn more about selfdestruct vulnerabilities →
VPS-005 — delegatecall Usage (HIGH)
What it detects: Any use of delegatecall in the contract source.
Why it matters: delegatecall executes code from another contract in the context of the calling contract — sharing its storage layout, its msg.sender, and its msg.value. If the target address of a delegatecall can be controlled by an attacker (through a user-supplied parameter, a compromised admin key, or an unprotected setter), the attacker can overwrite any storage slot in the calling contract, including ownership variables and token balances.
Fix: Ensure all delegatecall targets are immutable or behind strict access control. Never allow user-supplied addresses as targets. For upgradeable contracts, follow the EIP-1967 proxy pattern and use the OpenZeppelin Transparent or UUPS proxy implementation.
Learn more about delegatecall risks →
VPS-006 — Inline Assembly (MEDIUM)
What it detects: Any inline assembly block (assembly { ... }) in the contract source.
Why it matters: Inline assembly bypasses every Solidity safety mechanism — overflow checking, type checking, and memory management. Errors in assembly are invisible to the compiler and extremely difficult to audit. Even experienced Solidity developers introduce subtle bugs in assembly blocks, particularly around memory layout, return data handling, and free memory pointer management.
Fix: Use inline assembly only when it is strictly necessary for functionality or gas optimization not available in Solidity. All assembly blocks should be accompanied by detailed inline comments and should receive focused attention during any professional audit.
VPS-007 — ecrecover Malleability (MEDIUM)
What it detects: Any call to the raw ecrecover() precompile.
Why it matters: Elliptic curve signatures are mathematically malleable — for any valid signature (v, r, s), a second valid signature can be derived by setting v' = 1 - v and s' = secp256k1.n - s. If a contract tracks used signatures to prevent replay (storing the hash as "spent"), an attacker can submit the alternative form of the same signature as if it were a fresh authorization. Additionally, raw ecrecover returns address(0) on failure instead of reverting.
Fix: Use OpenZeppelin's ECDSA.recover(), which rejects malleable signatures and reverts (rather than returning zero) on failure. Always pair with a nonce and EIP-712 domain separator.
Learn more about signature replay attacks →
VPS-008 — Unbounded Loop (MEDIUM)
What it detects: A for loop that uses a dynamic array's .length as the upper bound.
Why it matters: If the array lives in storage and grows without a cap, the gas cost of iterating over it grows linearly with array size. Eventually, a transaction that calls the function exceeds the block gas limit and reverts — permanently. This makes the function uncallable without a contract upgrade, constituting a denial-of-service condition on that function.
Fix: Add an explicit maximum iteration cap, or implement pagination so the caller passes a start index and batch size. Consider replacing unbounded arrays with EnumerableSet from OpenZeppelin, which supports safe iteration patterns.
Learn more about DoS via gas limit →
VPS-009 — Unchecked send() Return Value (MEDIUM)
What it detects: Any call to address.send() that is not wrapped in a require().
Why it matters: Unlike transfer(), send() does not revert on failure — it returns false. If the return value is not checked, a failed ETH transfer is silently ignored. send() also forwards only 2,300 gas, which makes it fail on recipients with non-trivial fallback logic.
Fix: Migrate to addr.call{value: amount}("") with explicit return-value checking (VPS-001 pattern), or use OpenZeppelin's Address.sendValue(). If retaining send(), wrap it: require(payable(addr).send(amount), "Send failed").
Learn more about unchecked return values →
VPS-010 — Floating Pragma (LOW)
What it detects: A pragma solidity ^ version constraint (the caret indicates a floating version).
Why it matters: A floating pragma allows any compatible compiler version to be used. This means the bytecode produced in development (where a specific compiler is pinned) may differ from the bytecode produced in a different build environment. Different compiler versions can produce different gas costs, different stack depth behavior, and occasionally different semantic behavior for edge cases.
Fix: For production contracts, lock the pragma to an exact version: pragma solidity 0.8.24; instead of pragma solidity ^0.8.24;. This ensures reproducible builds and auditable bytecode.
VPS-011 — abi.encodePacked Hash Collision (MEDIUM)
What it detects: Any call to abi.encodePacked() in the contract source.
Why it matters: When abi.encodePacked() is called with multiple variable-length arguments, the packed encoding can produce the same bytes for different inputs. For example, abi.encodePacked("ab", "c") produces the same output as abi.encodePacked("a", "bc"). If the result is hashed and used to assert uniqueness (for allowlists, commitment schemes, or access control), two different inputs can produce the same hash, breaking the uniqueness assumption.
Fix: Use abi.encode() instead of abi.encodePacked() when hashing multiple dynamic-length values. abi.encode() includes length prefixes that eliminate collision ambiguity. If gas is critical, separate arguments with a fixed-length separator byte.
VPS-012 — Unchecked Arithmetic Block (LOW)
What it detects: Any unchecked { ... } block in the contract source.
Why it matters: Solidity 0.8+ added built-in overflow and underflow protection — arithmetic operations revert if they exceed the type's bounds. The unchecked block disables this protection for gas savings. If the arithmetic inside an unchecked block is not provably safe (mathematically impossible to overflow given the surrounding constraints), silent wrap-around can corrupt balance accounting or loop counters in ways that are extremely difficult to debug.
Fix: Restrict unchecked blocks to arithmetic that is demonstrably safe — the canonical example is a loop counter increment where the bound is already checked by the loop condition. Add an inline comment explaining precisely why overflow is impossible.
Learn more about integer overflow and underflow →
VPS-013 — Single-Step Ownership Transfer (MEDIUM)
What it detects: Any call to transferOwnership() without the use of Ownable2Step.
Why it matters: The standard OpenZeppelin Ownable.transferOwnership() transfers ownership in a single step. If the caller passes an incorrect address — a typo, an address on the wrong network, or address(0) — the transfer completes immediately and ownership is permanently lost. There is no confirmation step and no recovery path.
Fix: Upgrade to Ownable2Step (available in OpenZeppelin v4.9+). This requires the nominated new owner to explicitly call acceptOwnership(), turning a single-step transfer into a two-step handshake that prevents accidental permanent lockout.
Learn more about access control patterns →
VPS-014 — Missing Reentrancy Guard (HIGH)
What it detects: A contract that makes external calls (.call, .transfer, or .send) without importing or using ReentrancyGuard or the nonReentrant modifier.
Why it matters: Any function that makes an external call before updating all relevant state is potentially vulnerable to reentrancy — the external callee can call back into the original contract before the first invocation completes, reading or modifying state that is still in an intermediate, inconsistent form. This is the oldest and most consequential class of smart contract bugs.
Fix: Apply the Checks-Effects-Interactions (CEI) pattern: validate inputs, update all state, then make external calls — in that strict order. Additionally, inherit OpenZeppelin's ReentrancyGuard and add nonReentrant to every function that makes external calls or transfers value.
Learn more about reentrancy attacks →
VPS-015 — Hidden Internal ETH Transfer (MEDIUM)
What it detects: An internal call{value:} ETH transfer that has no proximate emit event. Three sub-cases are checked: (1) no emit anywhere in the contract; (2) a receive() function body with no emit; (3) individual call{value:} sites with no emit within ±5 lines.
Why it matters: Internal value transfers executed via call{value:} are invisible to off-chain indexers that rely on eth_getLogs — only transactions and events are captured in the standard log API. To reconstruct the full ETH flow through a contract, an indexer must use expensive execution trace analysis (debug_traceTransaction) rather than the standard log API. This creates a monitoring blind spot and makes fund-flow forensics significantly harder for users, protocol teams, and security researchers.
Fix: Emit a custom event at every internal ETH transfer site — for example, emit EtherSent(recipient, amount) before each call{value:}, and emit EtherReceived(msg.sender, msg.value) inside receive(). Alternatively, adopt the pull-payment pattern (OpenZeppelin PullPayment), which makes all ETH movements explicit and log-observable by design.
What the AI layer adds
Automated pattern matching catches a large and reproducible fraction of real-world bugs — but it has strict limits. It cannot detect logic errors, economic attack vectors, protocol-level design flaws, or vulnerabilities that only emerge when the contract is composed with another protocol (flash loan attacks, oracle manipulation, sandwich attacks).
The AI audit layer — powered by Claude — handles these higher-order risks. With the pre-scan results already embedded in the prompt, the AI focuses its reasoning on the most likely areas of concern rather than re-deriving what the pattern scanner already found. It also provides code-specific remediation guidance, a trust score, and a deploy-readiness verdict that accounts for the full context of the contract.
Run a free audit on your contract to see all 15 pre-scan patterns checked plus the full AI analysis — results in under 60 seconds, no signup required.