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.

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:

SWC-101: Integer Arithmetic Mythril found an execution path where arithmetic on a user-controlled input can overflow or underflow. In pre-0.8 contracts this is a direct vulnerability. In 0.8+ contracts, check whether the flagged arithmetic is inside an unchecked block.
SWC-105: Unprotected Ether Withdrawal (Ether Leak) Mythril found a path where ETH exits the contract to a caller-controlled address without adequate access control. This is frequently a true positive for missing 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:

  1. Borrow $100M USDC via flash loan (no capital required)
  2. Swap $100M USDC for Token X on Uniswap, driving Token X price up 10x
  3. Use the inflated Token X price to borrow far more than the real collateral value from the lending protocol
  4. Repay the flash loan from step 1
  5. 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:

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:

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.

  1. Scope and architecture review: Map all contracts, inheritance chains, external dependencies, and upgrade mechanisms before reading a single line of logic.
  2. Run Slither: Triage all High and Medium findings. Confirm or dismiss each one — never ignore without documented reasoning.
  3. 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.
  4. Access control audit: List every state-changing function. For each, verify: who can call it? Is that correct? Is the modifier using msg.sender not tx.origin?
  5. 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.
  6. Integer arithmetic: Audit all unchecked blocks and all type casts (especially narrowing casts). Verify mul-div ordering in fee calculations.
  7. 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.
  8. Proxy storage layout: If upgradeable, verify EIP-1967 slot usage. Check initializer protection. Verify storage layout compatibility between versions.
  9. 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.
  10. Front-running and MEV: Identify any function whose output depends on transaction ordering (DEX swaps, randomness, NFT mints). Assess economic incentive to front-run.
  11. 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.
  12. 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
← Back to Blog