Solidity Smart Contract Security Testing: Reentrancy, Flash Loans, and Audit Methodology
Smart contract security operates under constraints that have no analogue in conventional application security. A deployed Ethereum contract is immutable by default. Every line of bytecode is public. Every state variable is readable. Every function call executes on a globally replicated machine where anyone can observe the pending transaction pool before it confirms. And every exploitable vulnerability controls real money — sometimes hundreds of millions of dollars.
The consequences of these properties are not theoretical. The 2016 DAO hack extracted approximately $60M worth of ETH by exploiting a reentrancy vulnerability in withdrawal logic. The 2020 bZx attacks used uncollateralized flash loans to manipulate price oracles in the same transaction. Misconfigured access control in proxy initialization functions has drained protocol treasuries repeatedly. In each case, the vulnerability was a well-understood class of bug — but the immutability and public verifiability of the chain meant there was no patch, no rollback, no incident response playbook that could recover funds once they moved.
This guide covers the full smart contract audit workflow: static analysis with Slither, symbolic execution with Mythril, fuzz testing with Foundry, and the manual review patterns that automated tools consistently miss. The target audience is security engineers who already understand web application testing and need a rigorous entry point into EVM security.
Why Smart Contract Auditing Differs from Web App Pentesting
Web application testing assumes you can observe inputs and outputs, probe endpoints, and report findings that developers will patch before they matter. Smart contracts invert these assumptions entirely.
- Immutability: Unless a proxy upgrade pattern is in place, deployed bytecode cannot change. A vulnerability found after deployment is permanent unless the contract has a pause or migration mechanism.
- Public state: All on-chain data — including supposedly "private" state variables — is readable by anyone querying archive nodes. Secrets cannot live on-chain.
- Atomicity: Flash loans allow attackers to borrow unlimited capital, execute arbitrarily complex multi-protocol operations, and repay — all within a single transaction. Attack complexity that would require significant capital off-chain requires zero capital on-chain.
- Global visibility of pending transactions: The mempool is public. Transactions can be front-run, sandwiched, or back-run by MEV bots before they confirm.
- Composability as attack surface: DeFi protocols compose with each other. A price oracle vulnerability in Protocol A becomes an attack vector for Protocol B that relies on it.
The audit methodology must account for all of these. Automated tools catch a useful subset of vulnerability classes, but the highest-severity findings in most audits come from understanding how a protocol's economic incentives interact with its code paths — something that requires careful manual review.
Static Analysis with Slither
Slither is the industry-standard static analysis framework for Solidity, developed by Trail of Bits. It compiles contracts using the Solidity compiler, converts the output to an intermediate representation called SlithIR, and runs pattern-matching detectors against that IR. Because it operates on the compiler's output rather than raw source, it accurately handles inheritance, modifiers, and type resolution.
Installation and Basic Usage
# Install via pip (Python 3.8+)
pip3 install slither-analyzer
# Analyze a single file
slither contracts/Vault.sol
# Analyze a Hardhat or Foundry project
cd my-project
slither .
# Run specific detectors only
slither . --detect reentrancy-eth,reentrancy-no-eth,unprotected-upgrade
# Output JSON for CI pipeline integration
slither . --json slither-output.json
# Print a summary with impact and confidence levels
slither . --print human-summary
Key Detectors and What They Actually Flag
| Detector | What It Finds | Impact |
|---|---|---|
reentrancy-eth |
External calls before state updates where ETH can be drained | High |
reentrancy-no-eth |
External calls before state updates with no ETH value | Medium |
unprotected-upgrade |
UUPS upgrade functions missing access control | High |
suicidal |
Unprotected selfdestruct callable by anyone |
High |
weak-prng |
Randomness derived from block.timestamp or blockhash |
High |
tx-origin |
tx.origin used for authentication |
Medium |
arbitrary-send-eth |
ETH transfer to a caller-controlled address | High |
unchecked-transfer |
ERC20 transfer() return value not checked |
High |
divide-before-multiply |
Integer division truncation before multiplication | Medium |
Filtering False Positives and Writing Custom Detectors
Slither generates false positives, particularly for reentrancy findings in contracts that use OpenZeppelin's ReentrancyGuard in ways the detector does not recognize. Use inline comments to suppress known false positives:
// slither-disable-next-line reentrancy-no-eth
token.safeTransfer(recipient, amount);
For project-specific patterns — such as detecting all calls to a deprecated internal function — write a custom detector:
# custom_detectors/deprecated_call.py
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification
class DeprecatedCall(AbstractDetector):
ARGUMENT = "deprecated-call"
HELP = "Use of deprecated _unsafeTransfer function"
IMPACT = DetectorClassification.HIGH
CONFIDENCE = DetectorClassification.HIGH
def _detect(self):
results = []
for contract in self.compilation_unit.contracts_derived:
for function in contract.functions:
for node in function.nodes:
for call in node.internal_calls:
if call.name == "_unsafeTransfer":
results.append(self.generate_result([
"Deprecated _unsafeTransfer called in ",
function, "\n"
]))
return results
slither . --detect deprecated-call --detector-path custom_detectors/
Symbolic Execution with Mythril
Mythril approaches contract analysis differently from Slither. Rather than matching patterns against an IR, it performs symbolic execution: it explores execution paths through the EVM bytecode by substituting symbolic (unconstrained) values for inputs, then uses an SMT solver (Z3) to determine which concrete inputs would reach each path and whether any path violates a security property.
The key advantage over static analysis is that Mythril can reason about conditions. If a vulnerability requires a specific combination of state — for example, a balance that exceeds a threshold combined with a particular caller address — symbolic execution can determine whether that combination is reachable, and what inputs produce it.
Running Mythril
# Install
pip3 install mythril
# Analyze a local Solidity file
myth analyze contracts/Vault.sol --solv 0.8.19
# Analyze deployed bytecode from Etherscan
myth analyze -a 0xYOUR_CONTRACT_ADDRESS --rpc https://mainnet.infura.io/v3/YOUR_KEY
# Increase transaction depth (default: 2; higher finds more paths, slower)
myth analyze contracts/Vault.sol --transaction-count 3
# Output in JSON for tooling integration
myth analyze contracts/Vault.sol -o json
Understanding Mythril Output
Mythril reports findings by SWC (Smart Contract Weakness Classification) ID. The two most important categories:
unchecked block.
onlyOwner modifiers on withdrawal functions.
Mythril's primary limitation is state explosion: the number of paths grows exponentially with transaction depth and branch count. Deep DeFi protocols with many interacting functions often time out before analysis completes. Use --execution-timeout to cap analysis time, and combine with Slither's broader coverage for a complete picture.
# Limit execution time to 300 seconds
myth analyze contracts/Pool.sol --execution-timeout 300 --transaction-count 2
Reentrancy Attacks
Reentrancy occurs when a contract makes an external call to an untrusted address before updating its own state, and that untrusted address calls back into the original contract before the first execution frame completes. The canonical example is the DAO hack pattern:
// VULNERABLE: State updated after external call
contract VulnerableVault {
mapping(address => uint256) public balances;
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "Nothing to withdraw");
// External call BEFORE state update — reentrancy entry point
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
// State update comes AFTER — attacker re-enters before this runs
balances[msg.sender] = 0;
}
}
An attacker deploys a contract whose receive() function calls back into withdraw(). Because balances[msg.sender] has not yet been zeroed, each reentrant call passes the balance check and drains another amount of ETH:
// Reentrancy attacker contract (proof of concept)
contract ReentrancyAttacker {
VulnerableVault public target;
uint256 public attackAmount;
constructor(address _target) {
target = VulnerableVault(_target);
}
function attack() external payable {
attackAmount = msg.value;
target.deposit{value: msg.value}();
target.withdraw();
}
// Called by target on each ETH transfer
receive() external payable {
if (address(target).balance >= attackAmount) {
target.withdraw(); // Re-enter before state update
}
}
}
Cross-Function and Read-Only Reentrancy
Cross-function reentrancy is subtler: the reentrant call targets a different function that relies on the same state variable not yet updated. A contract might correctly guard its withdraw() with a mutex but leave borrow() unguarded, and borrow reads the same balances mapping.
Read-only reentrancy affects view functions. If Protocol B reads a price or balance from Protocol A via a view call, and Protocol A's state is temporarily inconsistent during an ongoing external call, Protocol B sees a corrupted value even though Protocol A itself is protected. This is the mechanism behind several Curve Finance-related exploits.
Fixes: Checks-Effects-Interactions and Reentrancy Guards
// FIXED: Checks-Effects-Interactions pattern
contract SecureVault {
mapping(address => uint256) public balances;
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "Nothing to withdraw");
// Effect BEFORE interaction
balances[msg.sender] = 0;
// Interaction last
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
// ALTERNATIVE: OpenZeppelin ReentrancyGuard mutex
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SecureVault is ReentrancyGuard {
mapping(address => uint256) public balances;
function withdraw() external nonReentrant {
uint256 amount = balances[msg.sender];
require(amount > 0, "Nothing to withdraw");
balances[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
On the question of transfer() vs call{value:}(): transfer() forwards only 2300 gas, which is insufficient for a reentrant call — but this is a mitigation, not a fix. The 2300 gas stipend is an implementation detail that can change, and it breaks legitimate use cases (contracts with non-trivial receive() logic). Use Checks-Effects-Interactions or nonReentrant and always use call{value:}().
Integer Overflow and Underflow
Before Solidity 0.8.0, arithmetic on unsigned integers silently wrapped around at type boundaries. Adding 1 to uint256(type(uint256).max) produced 0. Subtracting 1 from uint256(0) produced type(uint256).max. This is integer overflow/underflow, and it was the root cause of the BEC token hack that minted 57,896,044,618,658,097,711,785,492,504,343,953,926,634,992,332,820,282,019,728,792,003,956,564,819,967 tokens from nothing.
// VULNERABLE (Solidity < 0.8): overflow in balance update
function transfer(address to, uint256 amount) external {
// If balances[msg.sender] < amount, this underflows to a huge number
balances[msg.sender] -= amount;
balances[to] += amount;
}
Solidity 0.8.0+ adds built-in overflow/underflow checks that revert on violation. However, three patterns still introduce arithmetic vulnerabilities in 0.8+ code:
unchecked Blocks
// Intentional unchecked for gas optimization — verify bounds manually
function incrementCounter() external {
unchecked {
// Fine if counter is bounded elsewhere, dangerous if it can reach max
counter++;
}
}
Auditors must verify that every unchecked block either has mathematical proof that overflow is impossible, or has explicit bounds enforced immediately before entry.
Type Casting Truncation
// VULNERABLE: downcasting silently truncates in 0.8+
function recordAmount(uint256 largeAmount) external {
// If largeAmount > type(uint128).max, top bits are silently dropped
uint128 stored = uint128(largeAmount);
amounts[msg.sender] = stored;
}
// FIXED: use SafeCast
import "@openzeppelin/contracts/utils/math/SafeCast.sol";
function recordAmount(uint256 largeAmount) external {
uint128 stored = SafeCast.toUint128(largeAmount); // reverts on overflow
amounts[msg.sender] = stored;
}
Mul-Div Ordering
// VULNERABLE: division before multiplication loses precision
uint256 fee = (amount / 10000) * feeBps; // integer division truncates first
// CORRECT: multiply before dividing
uint256 fee = (amount * feeBps) / 10000; // full precision, then divide
Access Control Vulnerabilities
Access control failures are consistently one of the most common high-severity findings in smart contract audits. They range from completely missing modifiers to subtle authentication bypass patterns.
Missing Modifiers and Visibility Mistakes
// VULNERABLE: anyone can call drain()
contract Treasury {
address public owner;
constructor() { owner = msg.sender; }
// Missing onlyOwner modifier — any caller can drain funds
function drain(address recipient) external {
payable(recipient).transfer(address(this).balance);
}
}
// ALSO VULNERABLE: internal function exposed as public
function _recalculateShares(uint256 newTotal) public {
// Should be internal — exposed attack surface
totalShares = newTotal;
}
tx.origin Authentication Bypass
tx.origin is the original externally owned account (EOA) that initiated the transaction chain, whereas msg.sender is the immediate caller. When a contract uses tx.origin for authentication, a malicious intermediate contract can trick the legitimate owner into sending a transaction to it — and then call the target contract, which sees the owner's address as tx.origin:
// VULNERABLE: tx.origin phishing attack
contract VulnerableOwned {
address public owner;
modifier onlyOwner() {
require(tx.origin == owner, "Not owner"); // WRONG
_;
}
}
// CORRECT: always use msg.sender
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
Unprotected Initializers in Upgradeable Proxies
Upgradeable proxy patterns (OpenZeppelin Transparent Proxy, UUPS) use an initialize() function instead of a constructor, because constructors run on the implementation contract's context, not the proxy's. If this initialize() function is not protected against multiple calls, an attacker can call it after deployment and take ownership:
// VULNERABLE: initializer not protected
contract VulnerableImplementation {
address public owner;
function initialize(address _owner) external {
// No initializer guard — callable by anyone after deployment
owner = _owner;
}
}
// FIXED: use OpenZeppelin's Initializable
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract SecureImplementation is Initializable {
address public owner;
function initialize(address _owner) external initializer {
owner = _owner; // initializer modifier ensures single execution
}
}
Additionally, implementation contracts must always be initialized immediately after deployment. An uninitialized implementation contract at a known address can be self-destructed (on chains that still support it) or have its storage set to attacker-controlled values, which can affect the proxy through delegatecall.
Flash Loan Attacks and Oracle Manipulation
Flash loans are uncollateralized loans that must be borrowed and repaid within a single transaction. Protocols like Aave and Uniswap v3 offer them natively. From an attacker's perspective, they eliminate capital requirements: any exploit that can be executed in one atomic transaction can be executed with essentially unlimited liquidity.
The AMM Price Oracle Problem
Automated market makers (AMMs) like Uniswap price assets based on the current ratio of reserves: price = reserve_A / reserve_B. This spot price can be manipulated within a single transaction by swapping a large amount. If a lending protocol uses this spot price as its oracle, an attacker can:
- Borrow $100M USDC via flash loan (no capital required)
- Swap $100M USDC for Token X on Uniswap, driving Token X price up 10x
- Use the inflated Token X price to borrow far more than the real collateral value from the lending protocol
- Repay the flash loan from step 1
- Keep the excess borrowed funds as profit
The fix is to use a time-weighted average price (TWAP) oracle rather than spot price. TWAPs accumulate price data over many blocks, making single-transaction manipulation economically infeasible (it would require sustaining a manipulated price across multiple blocks at enormous cost):
// Using Uniswap v3 TWAP oracle (simplified)
function getTokenPrice(address pool, uint32 twapPeriod)
external view returns (uint256 price)
{
uint32[] memory secondsAgos = new uint32[](2);
secondsAgos[0] = twapPeriod; // e.g., 1800 seconds (30 minutes)
secondsAgos[1] = 0;
(int56[] memory tickCumulatives,) = IUniswapV3Pool(pool).observe(secondsAgos);
int56 tickCumulativeDelta = tickCumulatives[1] - tickCumulatives[0];
int24 averageTick = int24(tickCumulativeDelta / int56(uint56(twapPeriod)));
price = TickMath.getSqrtRatioAtTick(averageTick);
}
Identifying Flash Loan Attack Surface
During an audit, flag every function that reads an external price or balance and takes a significant action (minting, borrowing, liquidating) based on it. Ask: can the price input be controlled by the caller within a single transaction? If yes, the function has flash loan attack surface. Look specifically for:
- Calls to
balanceOf(address(this))or AMMgetReserves()used in pricing logic - Liquidation functions that determine collateral value at call time
- Mint functions where the minted amount depends on a ratio that can be manipulated
- Any function reading its own token balance to determine share prices (donation attacks)
Upgradeable Proxy Vulnerabilities
Proxy patterns introduce an entirely separate class of vulnerabilities around storage layout. In a proxy architecture, the proxy contract holds all state, and the implementation contract holds all logic. When the proxy delegatecalls the implementation, the implementation's code executes against the proxy's storage slots.
Storage Collision
If the proxy contract uses storage slot N for its own administrative variable (e.g., the implementation address), and the implementation contract also uses slot N for a user-facing variable, they collide. Writes to the "implementation address" slot from the implementation's perspective actually overwrite the proxy's stored implementation pointer — and vice versa.
// COLLISION RISK in naive proxy
contract NaiveProxy {
address implementation; // slot 0 — COLLIDES with slot 0 in implementation
fallback() external payable {
address impl = implementation;
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}
contract Implementation {
uint256 public totalSupply; // slot 0 — COLLIDES with proxy's implementation address
}
OpenZeppelin's transparent proxy solves this with EIP-1967, storing the implementation address at a pseudo-random slot derived from a hash: bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1). UUPS proxies push upgrade logic into the implementation itself and rely on the same slot convention. Audit all upgradeable contracts against EIP-1967 compliance and verify storage layout is preserved across upgrades using tools like @openzeppelin/upgrades-core.
Front-Running and MEV
Transaction ordering dependence (TOD) vulnerabilities occur when the outcome of a transaction depends on its position relative to other transactions in the same block. The mempool is public: any submitted transaction can be observed and a competing transaction submitted with higher gas price to be included first.
Common MEV (Maximal Extractable Value) patterns that security testers must evaluate:
- Sandwich attacks: A bot observes a large DEX swap, front-runs it (buys before, moving the price), lets the victim transaction execute at a worse price, then back-runs it (sells into the recovered price). The victim pays more than necessary; the attacker profits.
- Front-running NFT mints: Randomness determined at mint time (using block data) can be predicted. A bot can observe a pending mint transaction, see which token ID it would receive, and only submit its own mint when a valuable token ID is deterministic.
- Liquidation racing: Multiple bots race to be first to liquidate an undercollateralized position and claim the liquidation bonus.
Commit-Reveal Schemes
For applications where user intent must be secret until committed (randomness, sealed bids), use a commit-reveal scheme:
// Phase 1: user commits a hash of their choice
mapping(address => bytes32) public commits;
function commit(bytes32 hashedChoice) external {
commits[msg.sender] = hashedChoice;
}
// Phase 2: user reveals after commit phase closes
function reveal(uint256 choice, bytes32 salt) external {
bytes32 expectedHash = keccak256(abi.encodePacked(choice, salt, msg.sender));
require(commits[msg.sender] == expectedHash, "Invalid reveal");
// Process revealed choice
commits[msg.sender] = bytes32(0);
}
Testing with Foundry (Forge)
Foundry is the modern Solidity testing framework built by Paradigm. Unlike JavaScript-based Hardhat, Foundry tests are written in Solidity, run extremely fast, and include native support for fuzzing, invariant testing, and mainnet forking.
Fuzz Testing Arithmetic Edge Cases
// test/Vault.t.sol
import "forge-std/Test.sol";
import "../src/Vault.sol";
contract VaultFuzzTest is Test {
Vault vault;
function setUp() public {
vault = new Vault();
}
// Foundry calls this with hundreds of random `amount` values
function testFuzz_WithdrawNeverExceedsDeposit(uint256 amount) public {
// Bound amount to realistic range
amount = bound(amount, 1, 1000 ether);
address user = makeAddr("user");
vm.deal(user, amount);
vm.prank(user);
vault.deposit{value: amount}();
uint256 balanceBefore = address(vault).balance;
vm.prank(user);
vault.withdraw();
// Invariant: vault balance should never increase after a withdrawal
assertLe(address(vault).balance, balanceBefore);
// Invariant: user's vault balance should be zero after withdrawal
assertEq(vault.balances(user), 0);
}
}
Testing Access Control with vm.expectRevert
function test_OnlyOwnerCanDrain() public {
address attacker = makeAddr("attacker");
vm.prank(attacker);
// Expect the transaction to revert with the access control error
vm.expectRevert("Ownable: caller is not the owner");
vault.drain(attacker);
}
function test_OwnerCanDrain() public {
address owner = vault.owner();
vm.deal(address(vault), 1 ether);
vm.prank(owner);
vault.drain(owner); // Should succeed — no revert
assertEq(address(vault).balance, 0);
}
Mainnet Fork Tests for Flash Loan Scenarios
// Fork mainnet at a specific block for deterministic tests
function setUp() public {
// Requires MAINNET_RPC_URL in .env
vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), 19_500_000);
vault = new Vault(AAVE_V3_POOL, USDC_ADDRESS);
}
function test_FlashLoanOracleManipulation() public {
// Simulate attacker with flash loan
address attacker = makeAddr("attacker");
// Deal the attacker a large amount as if from flash loan repayment capacity
deal(USDC_ADDRESS, attacker, 100_000_000e6); // $100M USDC
vm.startPrank(attacker);
// Test whether the vault's pricing function can be manipulated
// by a large balance change in the same block
uint256 priceBefore = vault.getTokenPrice();
IERC20(USDC_ADDRESS).transfer(address(uniswapPool), 50_000_000e6);
uint256 priceAfter = vault.getTokenPrice();
// If TWAP is implemented correctly, price should not move significantly
assertApproxEqRel(priceBefore, priceAfter, 0.01e18); // <1% deviation
vm.stopPrank();
}
Audit Methodology Checklist
Execute audits in this order. Higher items on the list catch issues that invalidate assumptions lower items make.
- Scope and architecture review: Map all contracts, inheritance chains, external dependencies, and upgrade mechanisms before reading a single line of logic.
- Run Slither: Triage all High and Medium findings. Confirm or dismiss each one — never ignore without documented reasoning.
- Run Mythril (with 2-3 transaction depth on key contracts): Cross-reference findings with Slither. Mythril findings not caught by Slither are high-value candidates.
- Access control audit: List every state-changing function. For each, verify: who can call it? Is that correct? Is the modifier using
msg.sendernottx.origin? - Reentrancy review: Grep for all external calls (
.call{,.transfer(,IERC20.transfer,IERC20.transferFrom). For each, verify state is updated before the call or a reentrancy guard is in place. - Integer arithmetic: Audit all
uncheckedblocks and all type casts (especially narrowing casts). Verify mul-div ordering in fee calculations. - Oracle and price feed review: Identify all external data sources. Flag any spot price reads used in critical paths. Verify TWAP period is adequate for the protocol's value at risk.
- Proxy storage layout: If upgradeable, verify EIP-1967 slot usage. Check initializer protection. Verify storage layout compatibility between versions.
- Flash loan surface: Identify all functions that read a balance or price and take a value-significant action. Test whether that read can be manipulated within one transaction.
- Front-running and MEV: Identify any function whose output depends on transaction ordering (DEX swaps, randomness, NFT mints). Assess economic incentive to front-run.
- Foundry fuzz and invariant tests: Write invariant tests for core protocol properties (total assets ≥ total liabilities, share price monotonicity, etc.). Run with high fuzz depth.
- Economic logic review: Read the whitepaper or spec. Verify the code matches the spec. Look for edge cases at protocol limits: zero liquidity, maximum positions, dust amounts.
Remediation Patterns
| Vulnerability Class | Primary Remediation | Library / Reference |
|---|---|---|
| Reentrancy | Checks-Effects-Interactions + nonReentrant modifier |
OZ ReentrancyGuard |
| Integer overflow | Solidity 0.8+; SafeCast for narrowing; SafeMath for legacy | OZ SafeCast, SafeMath |
| Access control | Role-based modifiers; msg.sender not tx.origin |
OZ Ownable, AccessControl |
| Oracle manipulation | TWAP oracles; Chainlink price feeds with staleness check | Uniswap v3 OracleLibrary, Chainlink |
| Proxy storage collision | EIP-1967 slot conventions; storage gap pattern | OZ TransparentUpgradeableProxy, UUPS |
| Front-running | Commit-reveal; slippage protection; private mempools | Flashbots Protect RPC |
| Pull payment | Replace push payments with pull withdrawal pattern | OZ PullPayment |
| Weak randomness | Chainlink VRF for verifiable on-chain randomness | Chainlink VRF v2 |
Smart contract security requires a combination of automated tooling and deep manual review. Slither and Mythril catch the mechanical vulnerability classes reliably, but the highest-impact findings in real audits — economic attack vectors, logic errors in protocol invariants, and composability risks — require understanding the system's intended behavior well enough to find the gap between intent and implementation. Write the tests that prove the invariants hold, not just that the happy path works.
Smart contract vulnerabilities are permanent. Once a contract is deployed and exploited, funds are gone — there is no patch cycle, no rollback, no incident response that recovers on-chain value. The web APIs and backend services that interact with your contracts carry their own vulnerabilities: injection points, broken authentication, and input validation failures that attackers can exploit to manipulate what data reaches the chain.
Ironimo scans web APIs and application endpoints for the server-side vulnerabilities that interact with smart contracts — injection points, input validation failures, and API security issues that can be exploited by attackers to manipulate on-chain interactions. No manual setup required.
Start free scan