A smart contract is public, holds value directly and usually cannot be patched in place, so one flaw or one stolen admin key can empty it within a single block. The OWASP Smart Contract Top 10 (2025) cites over $1.42 billion lost across 149 incidents in its source data, and several of the largest recent losses came not from code bugs but from the privileged keys that control upgrades and parameters. This guide is for protocol engineers, security leads, auditors and the risk teams that approve on-chain deployments. It explains the main vulnerability classes at a conceptual level, the current taxonomies, a secure development lifecycle with named tools, the limits of audits, upgrade governance and admin key control, and monitoring. It is defensive: there is no exploit code and no attack walkthrough.
Key takeaways
- Use a maintained taxonomy. The SWC Registry states it "is no longer actively maintained"; use the OWASP Smart Contract Top 10 (2025) and the EEA EthTrust Security Levels for current coverage.
- Specify before you test. Fuzzing, invariant testing and formal verification are only as good as the properties you write, so the specification is a security artefact.
- Audits are a snapshot. SEAL notes that external reviews are "not a certificate of safety"; pair them with internal testing, bug bounties and re-review of every change.
- Admin keys are the biggest single risk. Upgrade, pause and parameter roles should sit behind a multisig and a timelock, with signers who verify calldata on trusted hardware.
- Plan for the bad day. Circuit breakers, a scoped pause role, live monitoring and a rehearsed response limit losses when prevention fails.
Why smart contracts need their own discipline
- Everything is visible: the Solidity documentation warns that "Everything you use in a smart contract is publicly visible", including variables marked
private. Secrets, randomness seeds and pending logic cannot be hidden on-chain. - Value sits in the code: a contract is both application and vault. Solidity's guidance is to "Restrict the amount of Ether (or other tokens) that can be stored in a smart contract" so a bug has a ceiling.
- Composability: contracts call other contracts, including untrusted ones, and borrow prices and liquidity from them. A safe contract can become unsafe when a dependency changes.
- Adversarial ordering: pending transactions are visible before inclusion, and block builders choose ordering, so front-running and sandwich trades (forms of maximal extractable value, MEV) are part of the threat model on the Ethereum Virtual Machine (EVM) and similar chains.
- Immutability cuts both ways: immutable code cannot be fixed; upgradeable code can be fixed, or replaced by whoever controls the upgrade key.
Vulnerability classes
The table maps each class to the OWASP 2025 ID, the legacy SWC ID where one exists, and the primary defence. Descriptions are conceptual.
| Class | What goes wrong | OWASP SC 2025 / SWC | Primary defence |
|---|---|---|---|
| Access control | A privileged function (mint, withdraw, upgrade, set price) is callable by the wrong party, or authorisation relies on tx.origin | SC01 / SWC-105 | Explicit role checks on every state-changing function, msg.sender not tx.origin, role review in audit scope |
| Price oracle manipulation | The contract trusts a price that can be moved, such as an AMM spot price in a thin pool | SC02 | Manipulation-resistant feeds, liquidity checks, staleness and deviation bounds |
| Flash loan amplification | Uncollateralised loans repaid in the same transaction give anyone temporary large capital to exploit a pricing or governance weakness | SC07 | Design as if every caller has unlimited capital for one transaction; avoid same-block price and vote reads |
| Logic errors | Reward, interest, rounding or liquidation maths does not match the intended economics | SC03 | Written specification, invariant tests, economic review |
| Missing input validation | Unchecked parameters, addresses or array lengths reach sensitive logic | SC04 | Validate at entry points; reject zero addresses and out-of-range values |
| Reentrancy | An external call lets the callee re-enter before state is updated | SC05 / SWC-107 | Checks-Effects-Interactions, reentrancy guards, pull-based withdrawals |
| Unchecked external calls | Return values of low-level calls are ignored, so failures pass silently | SC06 | Check every return value; prefer audited wrappers |
| Integer overflow and underflow | Arithmetic wraps or truncates, often inside unchecked blocks, casts or older compiler versions | SC08 / SWC-101 | Solidity 0.8+ checked arithmetic, minimal unchecked use, safe casting |
| Insecure randomness | Block data used as a random source can be predicted or influenced | SC09 | Verifiable randomness or commit-reveal schemes |
| Denial of service | Unbounded loops exceed the block gas limit, or one failing recipient blocks everyone | SC10 | Bounded iterations, pull payments, isolate per-user failures |
| Signature replay | A signed message is accepted twice, on another chain or by another contract | SWC-121 | Nonces, deadlines and an EIP-712 domain with chainId and verifyingContract |
| Unsafe delegatecall and upgrades | Code runs in the caller's storage context, or a proxy is left uninitialised or misconfigured | SWC-112 | Audited proxy patterns, locked implementations, storage layout checks |
Notes on the classes that cause the most damage
- Access control: ranked first in OWASP's 2025 list. Use a role-based model (OpenZeppelin's
AccessControlis a common choice) with one role per power rather than a single owner, deny by default, emit events on every role change so monitoring can see it, and add a unit test that calls each privileged function from an unprivileged account and expects a revert. Initialisers, migration helpers and "temporary" admin functions are frequent gaps. - Reentrancy: Solidity's rule is that "Interaction with other contracts should be the very last step in any function." It applies to any external call, not only Ether transfers, and across multiple contracts that share state. The 2016 DAO loss made the class famous; it still recurs in cross-contract and read-only forms.
- Integer issues: since Solidity 0.8.0 arithmetic is checked by default and "overflows will always revert", but the documentation warns "do not assume you are protected from overflow bugs": a revert can lock a contract, and
uncheckedblocks wrap silently. - Signature replay: EIP-712 states that it "does not include replay protection". Its domain fields (name, version, chainId, verifyingContract, salt) separate signing contexts, but the application must still reject a repeated message or make the action idempotent, which in practice means nonces and expiry.
- Oracles: Chainlink's own guidance tells integrators to "make sure that the asset has a sufficiently healthy level of liquidity", to detect "stale, delayed, missing, or unexpected values", and that developers "remain responsible for ensuring that protocol risk parameters are configured appropriately". Time-weighted averages, multiple sources and bounds reduce, but do not remove, manipulation risk.
- MEV and front-running: protect users with slippage limits and deadlines on trades, commit-reveal for auctions, and avoid logic that rewards being first.
Taxonomies and standards
| Reference | Status | Use it for |
|---|---|---|
| SWC Registry (SWC-100 to SWC-136) | Not maintained; "has not been thoroughly updated since 2020" and may contain "crucial omissions" | Legacy IDs still printed by older tools and audit reports |
| OWASP Smart Contract Top 10 (2025) | Current; built from SolidityScan Web3HackHub data and 2024 loss reports | Prioritising risks and training |
| EEA EthTrust Security Levels | Maintained; the SWC Registry points reviewers to it | Audit and review requirements |
| Smart Contract Security Verification Standard (SCSVS) | Maintained developer guidance, also referenced by the SWC Registry | Developer checklists and requirements |
A secure development lifecycle for contracts
The general secure development practices in NIST SP 800-218 (SSDF) apply. What changes is the weight on pre-deployment assurance, because post-deployment fixes are slow, public and sometimes impossible.
- Specify: write down what the system must always and never do: solvency, access rules, supply limits, who can pause or upgrade. These become invariants.
- Threat model: list privileged roles, external dependencies (oracles, tokens, bridges) and economic assumptions. SEAL's audit preparation guide asks teams to document "all access control roles and their privileges" and known centralisation risks.
- Build on audited components: use well-reviewed libraries for tokens, access control, reentrancy guards and proxies rather than custom code.
- Analyse statically on every commit: Slither, "a Solidity & Vyper static analysis framework written in Python3", ships 100 detectors and its own GitHub Action for code scanning; its
vars-and-authprinter lists which variables each function writes and what authorisation it checks. - Test, then fuzz: unit and integration tests, then property-based fuzzing. Foundry's invariant testing "verifies properties that should always hold true, regardless of the sequence of actions taken", with
runsanddepthset under[invariant]infoundry.tomland handlers to constrain inputs. Echidna, a Haskell fuzzer, checksechidna_properties or assertions and keeps a coverage-maximising corpus. - Formally verify critical paths: ethereum.org lists Certora Prover, the Solidity SMTChecker, solc-verify and KEVM as program verifiers, and Mythril, hevm and Manticore for symbolic execution. Its stated limits: verification needs "considerable manual labor" and a poor specification lets real bugs through.
- Freeze and review: fix the commit, run an external audit, fix findings, and have fixes re-reviewed.
- Deploy with guard rails: deposit caps, rate limits and a pause role, raised gradually as confidence grows.
| Technique | Example tools | Finds well | Misses |
|---|---|---|---|
| Static analysis | Slither, Semgrep smart contract rules | Known patterns: reentrancy shapes, unchecked calls, missing access modifiers | Economic and business logic flaws |
| Symbolic execution | Mythril, hevm, Manticore | Reachable assertion failures on bytecode paths | Deep multi-transaction state; slow on large systems |
| Fuzzing and invariants | Foundry, Echidna | Sequences that break stated invariants | Anything without a property written for it |
| Formal verification | Certora Prover, SMTChecker, KEVM | Proof that specified properties always hold | Properties nobody specified; off-chain and oracle behaviour |
| Manual audit | Independent reviewers | Design flaws, logic errors, privilege risks | Code changed after the review; time-boxed coverage |
Audits, contests and bug bounties
- Prepare properly: SEAL's preparation guide asks for a plain-English overview, flow diagrams, design decisions, known issues, external dependencies and role lists, plus prior runs of Slither, Mythril and Semgrep. A narrow, defined scope beats a sprawling one.
- Know the limits: SEAL states that "External reviews are not foolproof and cannot guarantee absolute security." An audit covers one commit for a fixed time; deployment scripts, configuration, admin keys and later upgrades are often out of scope unless you add them.
- Choose the format: a private audit suits a complex protocol needing deep design review; a competitive audit contest adds breadth for a fixed window; a standing bug bounty covers live code continuously. Larger protocols use all three; a small team shipping a simple contract may start with one private audit and a bounty sized to the value at risk.
- Disclose safely: publish a security contact and disclosure policy, and consider SEAL's Safe Harbor framework, which it describes as "Legal protection for whitehat rescues during active exploits."
Upgrades, timelocks and admin keys
Upgradeable contracts trade immutability for fixability. OpenZeppelin documents three main proxy patterns: Transparent (upgrades via a separate ProxyAdmin), UUPS (upgrade logic in the implementation, where _authorizeUpgrade "must be overridden to include access restriction") and Beacon (one upgrade changes every proxy pointing at the beacon). Proxy state sits in ERC-1967 storage slots to avoid layout collisions.
- Lock implementations: OpenZeppelin warns "Avoid leaving a contract uninitialized" and recommends calling
_disableInitializersin implementation constructors. - Do not mix patterns: a UUPS implementation behind a Transparent proxy "might allow non-admins to perform upgrade operations".
- Check storage layout: compare layouts between versions in CI before any upgrade is proposed.
- Put a timelock in front of power: OpenZeppelin's TimelockController separates Proposer, Executor, Canceller and Admin roles, and says the timelock should hold "any funds, ownership, and access control roles", with any setup admin told to "promptly renounce the role". SEAL adds that "Timelocks and review windows are intentional controls, not friction to bypass."
- Keep emergency powers narrow: a pause or cancel role may bypass the delay, but should not be able to upgrade code or move funds.
Where identity fits: privileged admin keys
Owner, upgrader, pauser, minter and oracle-setter roles are privileged non-human identities, and they are usually held by human signers on ordinary laptops. Radiant Capital's incident update (16 October 2024, approximately $50M) describes a DPRK-linked actor compromising "multiple developer devices", so that interfaces showed benign data "while malicious transactions were signed in the background", despite simulation and payload checks. Radiant reports that Mandiant attributed the attack to UNC4736.
- Inventory roles on-chain: list every address holding each role, what it can do, and who controls it. Remove deployer and test accounts after launch.
- Use two-step ownership transfer: patterns such as OpenZeppelin's
Ownable2Steprequire the new owner to accept, preventing transfer to a mistyped address. - Harden signers: dedicated signing devices, hardware wallets with clear signing, calldata decoded on a second independent tool, and same-day removal of departing signers. See the Privileged Access Management Guide and the Cryptographic Key Management Guide.
- Protect the deployment path: deployer keys and CI secrets that publish contracts or front-ends are as sensitive as the admin key. See the CI/CD Pipeline Identity Security Guide.
Decision guidance
| Choice | Prefer A when | Prefer B when |
|---|---|---|
| Immutable vs upgradeable | Logic is simple, well tested and holds high value long term | The protocol is young or complex and must be fixable, with timelock and multisig control |
| Transparent vs UUPS proxy | You want upgrade logic outside the implementation and accept higher deployment cost | You want cheaper proxies and can guarantee every implementation keeps a restricted _authorizeUpgrade |
| Short vs long timelock | Parameters need frequent tuning and a security council can cancel | Users need time to exit before any change to code or custody takes effect |
| Single audit vs layered assurance | Small, standard contract with modest value | Novel mechanism or high total value locked: multiple audits, contest, formal verification and bounty |
Monitoring and incident response
- Monitor: role grants and revocations, ownership transfers, upgrade proposals and executions, pauses, large withdrawals, oracle deviations, failed-transaction spikes and changes in dependent protocols.
- Circuit breakers: Chainlink describes breakers that react to "stale prices, drastic price changes, or prices approaching a predetermined min/max threshold" and a manual kill switch if an upstream contract is found vulnerable.
- Fail-safe mode: Solidity's guidance suggests a mode the contract enters automatically when self-checks fail.
- Respond: pause, alert users, coordinate with exchanges and stablecoin issuers, engage help such as SEAL 911, preserve evidence, and redeploy or upgrade only through the normal review path. See the Incident Response Guide.
Metrics that show it is working
- Share of state-changing functions covered by at least one invariant or fuzz property.
- Open high and medium findings from static analysis and audits, and median days to fix.
- Privileged roles held by an externally owned account rather than a multisig or timelock (target zero).
- Timelock delay on upgrades against the time users need to exit.
- Minutes from a role change or large outflow on-chain to human acknowledgement.
- Days from a signer leaving to removal from every multisig.
Rolling out the programme
- Baseline: inventory deployed contracts, proxies, roles and holders; run Slither across all repositories and triage findings.
- Move power behind controls: transfer owner and upgrade roles to a multisig, then to a timelock the multisig controls. Test on a testnet or fork first; keep the previous owner able to accept back until the new path has executed a no-op change.
- Add property testing: start with five to ten core invariants such as solvency and supply; run them in CI, then expand.
- Introduce monitoring in alert-only mode: tune thresholds for several weeks before wiring alerts to automated pausing; revert automated actions to alert-only if they fire on legitimate activity.
- Layer external assurance: audit before each major release, launch a bounty, and drill the pause and upgrade runbooks twice a year.
Common failure patterns
- Treating the audit as the finish line: code, configuration or dependencies change after review and nobody re-reviews.
- A single key with god-mode: upgrade, mint and withdraw rights held by one externally owned account, often the deployer.
- Trusting a spot price: reading an AMM pool price in the same transaction that acts on it.
- Uninitialised proxies and implementations: leaving
initializecallable by anyone. - Signers who trust the screen: as at Radiant and the Bybit hack, where tampered interfaces led to approval of malicious transactions.
- Stale signer and role lists: former staff or decommissioned services still holding roles.
Framework mapping
| Area | NIST CSF 2.0 | NIST SP 800-53 Rev. 5 | CIS Controls v8.1 | ISO/IEC 27001:2022 Annex A |
|---|---|---|---|---|
| Secure development and testing | PR.PS | SA-11, SA-11(1), SA-15 | 16.12 | 8.25, 8.28, 8.29 |
| Upgrades and change control | PR.PS | CM-3 | 16.1 | 8.32 |
| Admin keys and roles | PR.AA | AC-5, AC-6, SC-12 | 5.4, 6.5 | 8.2, 8.24 |
| Bounties and disclosure | ID.RA | RA-5(11) | 16.2 | 8.8 |
| Monitoring and response | DE.CM, RS.MA | SI-4, IR-4 | 17.4 | 8.16, 5.26 |
Practitioner checklist
- Map findings and training to the OWASP Smart Contract Top 10 (2025), not the unmaintained SWC Registry.
- Write a specification with explicit invariants before code freeze.
- Run Slither in CI and fail builds on new high-impact findings.
- Run Foundry invariant tests or Echidna properties for solvency, supply and access rules.
- Formally verify the highest-value paths where the team can write sound specifications.
- Re-review every change after an audit, and run a bug bounty sized to value at risk.
- Hold upgrade, pause and parameter roles in a multisig behind a TimelockController; renounce setup admin roles.
- Lock implementations with
_disableInitializersand check storage layout before upgrades. - Use nonces, deadlines and an EIP-712 domain for every signed message.
- Monitor role, upgrade and outflow events 24/7 with a drilled pause runbook.
Standards and references
- OWASP: Smart Contract Top 10
- Solidity Documentation: Security Considerations
- SWC Registry: Smart Contract Weakness Classification
- Ethereum Improvement Proposals: EIP-712 Typed Structured Data Hashing and Signing
- ethereum.org: Formal Verification of Smart Contracts
- OpenZeppelin: Proxies
- Crytic: Slither Static Analyzer
- Security Alliance (SEAL): External Security Reviews Framework
Related NHI Mgmt Group resources: Privileged Access Management Guide · Cryptographic Key Management Guide · Vulnerability Management Guide · Open Source Security Guide · Crypto Scam and Drainer Guide