DEV Community

PandaSecurity
PandaSecurity

Posted on Originally published at panda-security.xyz

Smart Contract Security Checklist for Web3 Developers

Originally published at Smart Contract Security Checklist for Web3 Developers.

A smart contract can look simple in a code editor and still hold millions of dollars in user funds.

That is one of the uncomfortable realities of Web3 development. A function may only be 20 lines long, but a small mistake in accounting, permissions, token handling, or price calculations can become an exploitable path once the contract is deployed.

The numbers make the risk difficult to ignore.

Immunefi reported more than $1.46 billion lost to hacks across 192 incidents in 2024. CertiK reported $3.35 billion in losses across Web3 security incidents in 2025, although the 2025 figure was heavily affected by several exceptionally large attacks.

This does not mean every smart contract needs an enormous security team.

It does mean that security should be considered during development rather than added immediately before deployment.

This checklist covers the areas Web3 developers should review before putting a smart contract into production.


Smart Contract Security Checklist

Before deployment, make sure you can answer "yes" to the following questions:

  • [ ] Are all privileged functions properly protected?
  • [ ] Can an attacker manipulate an oracle or price source?
  • [ ] Are external calls handled safely?
  • [ ] Have reentrancy paths been considered?
  • [ ] Are token balances and accounting calculated correctly?
  • [ ] Are ERC-20 edge cases handled?
  • [ ] Are flash-loan-assisted attacks possible?
  • [ ] Are upgrade and initialization functions protected?
  • [ ] Are signatures and replay protection implemented correctly?
  • [ ] Are emergency functions restricted and tested?
  • [ ] Have important invariants been tested?
  • [ ] Has the contract been tested with malicious inputs?
  • [ ] Have dependencies and compiler versions been reviewed?
  • [ ] Is the deployment configuration secure?
  • [ ] Is there a monitoring and incident-response plan?

The sections below explain why each item matters.


1. Review Access Control First

Access control is one of the highest-impact areas in smart contract security.

OWASP's 2025 Smart Contract Top 10 places Access Control Vulnerabilities at #1. Its analysis of 2024 incidents attributed approximately $953.2 million in losses to access-control vulnerabilities.

That makes permission checks one of the first things developers should review.

Start by identifying every function that can change important protocol state.

For example:

function mint(address to, uint256 amount) external {
    _mint(to, amount);
}
Enter fullscreen mode Exit fullscreen mode

This may look harmless, but if mint() is supposed to be an administrative operation, anyone can call it.

A safer implementation might be:

function mint(address to, uint256 amount)
    external
    onlyRole(MINTER_ROLE)
{
    _mint(to, amount);
}
Enter fullscreen mode Exit fullscreen mode

But adding onlyRole() is not the end of the review.

You also need to understand:

  • Who receives the role?
  • Who can grant the role?
  • Who can revoke it?
  • Can the role administrator be changed?
  • Can the owner replace the role manager?
  • Can an upgrade bypass the access-control system?
  • Is the privileged account an EOA or multisig?

Ethereum's security documentation recommends carefully designing access control and notes that role-based access control and multisig administration can reduce single points of failure.

Access-control checklist

Review every function that can:

  • Mint or burn tokens
  • Transfer protocol-owned funds
  • Change fees
  • Change oracle addresses
  • Change implementation contracts
  • Pause or unpause the protocol
  • Change collateral parameters
  • Change liquidation parameters
  • Modify trusted contracts
  • Withdraw accumulated fees
  • Upgrade contracts

A useful audit question is:

"What happens if an attacker obtains this role?"

If the answer is "the attacker can drain the protocol", that role deserves particularly strong protection.


2. Check Upgradeability Carefully

Upgradeable contracts introduce another layer of security assumptions.

A proxy system may separate:

User
  ↓
Proxy
  ↓
Implementation
Enter fullscreen mode Exit fullscreen mode

That means the security of the application depends not only on the implementation contract but also on the proxy and upgrade mechanism.

Check:

  • Who controls the proxy?
  • Who can upgrade the implementation?
  • Is the upgrade authority a multisig?
  • Is there a timelock?
  • Can the implementation be initialized again?
  • Can an attacker call initialization directly?
  • Are storage layouts compatible?
  • Can an upgrade change critical permissions?
  • Is there an emergency upgrade mechanism?

A common mistake is focusing only on the business logic while ignoring the upgrade path.

For example:

function upgradeTo(address newImplementation)
    external
{
    implementation = newImplementation;
}
Enter fullscreen mode Exit fullscreen mode

If this function is not properly protected, the entire security model of the protocol can collapse.

OpenZeppelin specifically recommends treating upgrade mechanisms and administrative privileges as important parts of the security architecture.


3. Protect Initializers

Upgradeable contracts often use an initializer instead of a constructor.

For example:

function initialize(address admin) external {
    owner = admin;
}
Enter fullscreen mode Exit fullscreen mode

If this function can be called more than once, an attacker may be able to take control of the contract.

A common pattern is:

function initialize(address admin)
    external
    initializer
{
    owner = admin;
}
Enter fullscreen mode Exit fullscreen mode

But developers should still verify the complete initialization flow.

Check:

  • Can initialization happen twice?
  • Can an attacker initialize an implementation contract?
  • Is the proxy initialized immediately after deployment?
  • Are all inherited contracts initialized?
  • Can initialization be front-run?
  • Are initialization parameters validated?

Initialization bugs are especially dangerous because they can effectively turn deployment into an ownership race.


4. Review Reentrancy

Reentrancy is one of the classic smart contract vulnerabilities, and it remains relevant today.

The basic problem occurs when a contract makes an external call before updating its own state.

Consider:

function withdraw(uint256 amount) external {
    require(balance[msg.sender] >= amount);

    payable(msg.sender).call{value: amount}("");

    balance[msg.sender] -= amount;
}
Enter fullscreen mode Exit fullscreen mode

An attacker-controlled contract can potentially call withdraw() again before the balance is updated.

The safer pattern is generally:

function withdraw(uint256 amount) external {
    require(balance[msg.sender] >= amount);

    balance[msg.sender] -= amount;

    (bool success, ) =
        payable(msg.sender).call{value: amount}("");

    require(success);
}
Enter fullscreen mode Exit fullscreen mode

This follows the Checks-Effects-Interactions pattern.

Ethereum's official security documentation still recommends this approach when dealing with external calls and reentrancy.

But don't only search for call()

Modern reentrancy reviews should also consider:

  • ERC-777 hooks
  • ERC-721 callbacks
  • ERC-1155 callbacks
  • ERC-4626 interactions
  • Flash-loan callbacks
  • DEX callbacks
  • Cross-contract calls
  • Read-only reentrancy

A contract does not need to explicitly call:

msg.sender.call(...)
Enter fullscreen mode Exit fullscreen mode

to have a reentrancy problem.

The important question is:

"Can control flow leave the current contract before critical state is finalized?"


5. Review External Calls

External calls are trust boundaries.

Whenever your contract interacts with another contract, ask:

What contract am I calling?
Who controls it?
What can it call back?
What assumptions am I making about its return value?
Can it revert?
Can it consume excessive gas?
Can it manipulate state before returning?
Enter fullscreen mode Exit fullscreen mode

For example:

token.transfer(user, amount);
Enter fullscreen mode Exit fullscreen mode

is not necessarily equivalent to:

SafeERC20.safeTransfer(token, user, amount);
Enter fullscreen mode Exit fullscreen mode

ERC-20 implementations are not always perfectly consistent.

Some tokens return false.

Some return no value.

Some have unusual transfer behavior.

Using battle-tested libraries reduces the amount of token-specific behavior your protocol needs to handle itself. Ethereum's security guidance recommends using well-tested libraries rather than repeatedly implementing common primitives from scratch.


6. Don't Assume Every ERC-20 Behaves the Same

One of the most common mistakes in DeFi development is treating ERC-20 as if every token follows exactly the same behavior.

Your contract may interact with tokens that have:

  • Fee-on-transfer mechanics
  • Rebasing
  • Transfer restrictions
  • Blacklists
  • Pausable transfers
  • Non-standard return values
  • Decimals other than 18
  • Upgradeable implementations
  • Callback-like behavior

Consider:

uint256 before = token.balanceOf(address(this));

token.transferFrom(
    msg.sender,
    address(this),
    amount
);

uint256 received = amount;
Enter fullscreen mode Exit fullscreen mode

The contract assumes it received exactly amount.

That assumption may not always hold.

A safer accounting model may measure the actual balance difference:

uint256 before = token.balanceOf(address(this));

token.safeTransferFrom(
    msg.sender,
    address(this),
    amount
);

uint256 received =
    token.balanceOf(address(this)) - before;
Enter fullscreen mode Exit fullscreen mode

Whether this approach is appropriate depends on the protocol's intended token model, but the important point is to make the assumption explicit.


7. Check Oracle Security

Oracle manipulation has become one of the most important DeFi attack surfaces.

OWASP's 2025 Smart Contract Top 10 ranks Price Oracle Manipulation #2.

Any protocol using prices should answer:

  • Where does the price come from?
  • Can users influence the price?
  • Is the source based on a single pool?
  • What liquidity does the pool have?
  • Is the price based on spot or historical data?
  • Is a TWAP being used?
  • What happens when the oracle becomes stale?
  • What happens if the oracle returns zero?
  • What happens if the price moves significantly in one block?

A dangerous pattern is relying directly on a low-liquidity AMM spot price.

For example:

uint256 price =
    reserve1 * 1e18 / reserve0;
Enter fullscreen mode Exit fullscreen mode

If an attacker can temporarily move the reserves, the protocol may use a manipulated price.

The problem becomes much more serious when the manipulated price controls:

  • Borrowing capacity
  • Liquidation prices
  • Collateral valuation
  • Share minting
  • Redemption amounts
  • LP token valuation

8. Review Flash-Loan Attack Paths

Flash loans do not create vulnerabilities by themselves.

They make existing vulnerabilities much easier to exploit.

An attacker can borrow a large amount of capital, manipulate temporary protocol state, execute an exploit, and repay the loan within the same transaction.

When reviewing a DeFi protocol, ask:

"What can an attacker do if they temporarily have $100 million?"

Then test:

  • Price manipulation
  • Governance voting
  • Collateral deposits
  • Liquidations
  • Reward calculations
  • Share-price manipulation
  • LP token pricing
  • Reserve manipulation
  • Temporary liquidity attacks

OWASP includes Flash Loan Attacks in its 2025 Smart Contract Top 10.

The important thing is not simply checking whether the protocol supports flash loans.

The real question is whether protocol logic incorrectly assumes that capital must come from the attacker's own wallet.


9. Check Accounting Invariants

Many serious DeFi vulnerabilities are not obvious "Solidity bugs".

They are accounting bugs.

For example:

shares = assets * totalShares / totalAssets;
Enter fullscreen mode Exit fullscreen mode

may look mathematically correct.

But what happens when:

totalAssets = 0
Enter fullscreen mode Exit fullscreen mode

or:

totalShares = 0
Enter fullscreen mode Exit fullscreen mode

or:

assets * totalShares
Enter fullscreen mode Exit fullscreen mode

overflows?

Even when Solidity prevents arithmetic overflow, incorrect ordering and rounding can still create exploitable economic behavior.

For every financial function, define the invariant first.

For example:

Total user balances <= protocol assets
Enter fullscreen mode Exit fullscreen mode

or:

Shares minted <= proportional contribution
Enter fullscreen mode Exit fullscreen mode

or:

Debt cannot become negative
Enter fullscreen mode Exit fullscreen mode

Then test whether an attacker can violate the invariant.

This approach is often more productive than simply searching for known vulnerability patterns.


10. Pay Special Attention to ERC-4626 Vaults

ERC-4626 vaults deserve their own checklist.

Review:

deposit()
mint()
withdraw()
redeem()
previewDeposit()
previewMint()
previewWithdraw()
previewRedeem()
convertToShares()
convertToAssets()
Enter fullscreen mode Exit fullscreen mode

Then compare the accounting between these functions.

Important questions include:

  • Can the first depositor manipulate the share price?
  • Can donations change the exchange rate?
  • Are previews consistent with execution?
  • Are rounding directions correct?
  • Can assets be lost due to rounding?
  • Are fees included consistently?
  • Can totalAssets() be manipulated?
  • Can external yield change the accounting unexpectedly?
  • Are pending withdrawals handled correctly?

For vaults, it is often useful to test sequences rather than individual calls:

deposit
→ donate
→ deposit
→ withdraw
→ redeem
Enter fullscreen mode Exit fullscreen mode

Many accounting vulnerabilities only appear after several state transitions.


11. Validate All User Inputs

Never assume users will provide reasonable parameters.

Check:

require(amount > 0);
require(receiver != address(0));
require(deadline >= block.timestamp);
require(price > 0);
Enter fullscreen mode Exit fullscreen mode

depending on the application.

Look for:

  • Zero addresses
  • Zero amounts
  • Extremely large values
  • Duplicate IDs
  • Invalid enum values
  • Expired deadlines
  • Invalid signatures
  • Invalid array lengths
  • Unexpected array ordering
  • Unsupported tokens
  • Unsupported chains

OWASP's 2025 ranking places Lack of Input Validation at #4.

A good rule is:

Validate assumptions at the boundary where the value enters the system.

Don't allow invalid state to propagate through several contracts and only fail later.


12. Check Signature Verification

Protocols using off-chain signatures should review:

Who signed?
What exactly was signed?
Which chain?
Which contract?
What nonce?
What deadline?
Can the signature be replayed?
Enter fullscreen mode Exit fullscreen mode

For example, a signed message should generally bind itself to the intended domain.

Important parameters often include:

chainId
verifyingContract
nonce
deadline
Enter fullscreen mode Exit fullscreen mode

Without replay protection, a valid signature can potentially be reused.

Also review:

  • EIP-712 domain separation
  • ERC-1271 contract signatures
  • Signature malleability
  • Nonce management
  • Deadline handling
  • Cross-chain replay

Do not assume:

ecrecover(...)
Enter fullscreen mode Exit fullscreen mode

alone provides a complete signature-security model.


13. Be Careful With tx.origin

Avoid using:

require(tx.origin == owner);
Enter fullscreen mode Exit fullscreen mode

for authorization.

tx.origin represents the original transaction initiator, not necessarily the contract directly interacting with your contract.

For access control, msg.sender is normally the appropriate primitive:

require(msg.sender == owner);
Enter fullscreen mode Exit fullscreen mode

Even then, the overall permission architecture still needs to be reviewed.


14. Review Denial-of-Service Risks

Security is not only about stealing money.

An attacker may also make an important function unusable.

Common examples include:

for (uint256 i = 0; i < users.length; i++) {
    ...
}
Enter fullscreen mode Exit fullscreen mode

If users.length can grow without bounds, eventually the function may exceed the block gas limit.

Review:

  • Unbounded loops
  • Large arrays
  • User-controlled iteration
  • Failed external calls
  • DoS through reverting receivers
  • Forced ETH
  • Storage growth
  • Queue processing

OWASP includes Denial of Service in its smart contract security categories.


15. Don't Ignore selfdestruct, ETH Transfers, and Forced ETH

Developers sometimes assume:

address(this).balance
Enter fullscreen mode Exit fullscreen mode

exactly represents the ETH deposited through the protocol.

That assumption can be wrong.

ETH can be forced into a contract through mechanisms outside the normal deposit flow.

Therefore, accounting should generally not depend blindly on:

address(this).balance
Enter fullscreen mode Exit fullscreen mode

when exact internal accounting is required.

Instead, distinguish between:

Protocol accounting
Enter fullscreen mode Exit fullscreen mode

and:

Actual blockchain balance
Enter fullscreen mode Exit fullscreen mode

and explicitly decide how unexpected funds should be handled.


16. Review Compiler and Dependency Versions

The Solidity version matters.

Before deployment, check:

Solidity version
OpenZeppelin version
External libraries
Compiler settings
Optimizer settings
Proxy tooling
Build configuration
Enter fullscreen mode Exit fullscreen mode

Do not blindly copy an old contract and change:

pragma solidity ...
Enter fullscreen mode Exit fullscreen mode

without understanding the differences between compiler versions.

Also avoid unnecessary custom implementations of common primitives.

For example, if a well-tested library already provides:

ERC20
AccessControl
SafeERC20
ReentrancyGuard
ECDSA
Math
Enter fullscreen mode Exit fullscreen mode

there is usually little value in rewriting them unless there is a specific reason.

Ethereum's security guidance similarly recommends well-tested libraries and dependency management.


17. Write Tests for Attackers, Not Just Users

A normal unit test might look like:

Alice deposits 100 ETH
Alice withdraws 100 ETH
Enter fullscreen mode Exit fullscreen mode

That proves the happy path works.

Security tests should look more like:

Attacker deposits 1 wei
Attacker manipulates price
Attacker calls function twice
Attacker uses a flash loan
Attacker passes address(0)
Attacker reenters callback
Attacker uses an expired signature
Attacker calls function from an unauthorized account
Enter fullscreen mode Exit fullscreen mode

The difference is important.

A good security test suite should deliberately try to break the protocol.


18. Test Invariants and Properties

Unit tests answer:

"Does this example work?"

Invariant tests answer:

"Can this important property ever become false?"

For example:

sum(userBalances) <= totalAssets
Enter fullscreen mode Exit fullscreen mode

or:

totalSupply cannot decrease unexpectedly
Enter fullscreen mode Exit fullscreen mode

or:

only authorized addresses can modify protocol parameters
Enter fullscreen mode Exit fullscreen mode

Tools such as fuzzing and invariant testing can explore many combinations that manually written test cases will never cover.

Ethereum's security guidance specifically recommends extensive testing and tools such as Slither, Echidna, and Manticore as part of the security process.


19. Run Static Analysis — But Don't Stop There

Static analyzers are useful for catching common patterns.

For example:

slither .
Enter fullscreen mode Exit fullscreen mode

can identify issues involving:

  • Reentrancy
  • Dangerous calls
  • Access control
  • Unused return values
  • Shadowing
  • Incorrect Solidity patterns
  • Uninitialized variables

But a clean Slither report does not mean the protocol is secure.

Static analysis is much better at finding:

known code patterns
Enter fullscreen mode Exit fullscreen mode

than:

incorrect business logic
Enter fullscreen mode Exit fullscreen mode

A contract can pass static analysis and still have a critical economic vulnerability.


20. Perform Manual State-Transition Analysis

This is where experienced security review becomes especially valuable.

Instead of reading functions independently, follow how state changes.

For example:

deposit()
    ↓
mint shares
    ↓
accrue yield
    ↓
borrow()
    ↓
liquidate()
    ↓
repay()
    ↓
withdraw()
Enter fullscreen mode Exit fullscreen mode

Ask:

"Can I reach an unexpected state by combining these functions?"

Many vulnerabilities exist not inside one function, but in the interaction between several apparently safe functions.

This is especially important for:

  • Lending protocols
  • Staking systems
  • Vaults
  • AMMs
  • Restaking protocols
  • Governance systems
  • Cross-chain bridges

21. Review Deployment Configuration

A secure contract can still be deployed insecurely.

Before mainnet deployment, verify:

  • Correct chain ID
  • Correct token addresses
  • Correct oracle addresses
  • Correct admin address
  • Correct proxy implementation
  • Correct initialization parameters
  • Correct fee configuration
  • Correct multisig
  • Correct ownership transfer
  • Correct pause configuration

A surprisingly common failure mode is deploying correct code with incorrect parameters.

Create a deployment checklist and verify every address before sending the transaction.


22. Verify Contracts on the Block Explorer

After deployment:

Deploy
↓
Initialize
↓
Verify source
↓
Check ownership
↓
Check implementation
↓
Check roles
↓
Check configuration
Enter fullscreen mode Exit fullscreen mode

Source verification makes the deployed bytecode easier to inspect and allows other researchers to review the actual production contract.

Do not assume the deployment script succeeded simply because the transaction succeeded.

Verify the resulting state on-chain.


23. Monitor the Contract After Deployment

Security does not end when the contract is deployed.

Monitor events and important state changes such as:

OwnershipTransferred
RoleGranted
RoleRevoked
Upgraded
Paused
Unpaused
OracleUpdated
FeeUpdated
Large withdrawals
Large deposits
Unexpected transfers
Enter fullscreen mode Exit fullscreen mode

Ethereum's security guidance also recommends monitoring deployed contracts and having an incident-response plan.

A vulnerability discovered quickly is much easier to contain than one discovered after an attacker has drained the protocol.


24. Keep an Emergency Response Plan

Every protocol handling meaningful value should know what happens after an exploit.

For example:

Suspicious transaction
        ↓
Alert
        ↓
Security team investigation
        ↓
Pause if appropriate
        ↓
Identify affected contracts
        ↓
Assess attacker access
        ↓
Contact infrastructure / exchanges / security teams
        ↓
Prepare remediation
Enter fullscreen mode Exit fullscreen mode

The exact process depends on the protocol.

The important thing is to decide before the incident happens.


A Practical Pre-Deployment Checklist

Here is a shorter version you can keep next to your codebase.

Architecture

  • [ ] Attack surface documented
  • [ ] Trust assumptions documented
  • [ ] External dependencies identified
  • [ ] Upgrade architecture reviewed
  • [ ] Admin architecture reviewed

Access Control

  • [ ] All privileged functions identified
  • [ ] onlyOwner / roles verified
  • [ ] Role administrators reviewed
  • [ ] Ownership transfer tested
  • [ ] Multisig configuration verified
  • [ ] Timelock considered for sensitive operations

DeFi Logic

  • [ ] Oracle manipulation tested
  • [ ] Flash-loan attack paths tested
  • [ ] Liquidation logic tested
  • [ ] Interest calculations tested
  • [ ] Share accounting tested
  • [ ] Rounding reviewed
  • [ ] Donation attacks considered

Token Handling

  • [ ] ERC-20 return values handled
  • [ ] Fee-on-transfer behavior considered
  • [ ] Rebasing tokens considered
  • [ ] Decimals handled correctly
  • [ ] Zero address checks implemented
  • [ ] Token approval flow reviewed

External Calls

  • [ ] Reentrancy reviewed
  • [ ] Callbacks reviewed
  • [ ] Return values checked
  • [ ] Untrusted contracts identified
  • [ ] DoS through external calls considered

Upgradeability

  • [ ] Proxy admin secured
  • [ ] Implementation verified
  • [ ] Initializer protected
  • [ ] Storage layout checked
  • [ ] Upgrade authorization tested
  • [ ] Upgrade events monitored

Testing

  • [ ] Unit tests
  • [ ] Fuzz tests
  • [ ] Invariant tests
  • [ ] Negative tests
  • [ ] Access-control tests
  • [ ] Reentrancy tests
  • [ ] Economic attack tests
  • [ ] Static analysis

Deployment

  • [ ] Correct chain
  • [ ] Correct addresses
  • [ ] Correct oracle
  • [ ] Correct admin
  • [ ] Correct initialization
  • [ ] Contract verified
  • [ ] Roles verified
  • [ ] Monitoring enabled
  • [ ] Incident response prepared

The Most Important Part of Smart Contract Security

A checklist is useful, but it should not become a substitute for understanding the protocol.

The most dangerous bugs are often not obvious Solidity mistakes.

They are usually assumptions such as:

"This price cannot be manipulated."

"This function can only be called by the frontend."

"The token always transfers exactly the requested amount."

"Nobody can borrow enough money to do this."

"The owner would never abuse this function."

"This callback cannot reenter."

"This value can never be zero."

"This contract will always be initialized correctly."
Enter fullscreen mode Exit fullscreen mode

An attacker does not need to respect these assumptions.

They only need to find one that the code fails to enforce.

That is why a strong smart contract security review combines automated analysis, adversarial testing, economic reasoning, and manual code review.


Final Thoughts

Smart contract security is not a single step at the end of development.

It starts with architecture, continues through implementation and testing, and remains important after deployment.

The current security data makes that clear. OWASP's 2025 Smart Contract Top 10 puts access control, oracle manipulation, logic errors, input validation, and reentrancy among the major smart contract risks.

For developers, the practical approach is straightforward:

Define the security assumptions.

Turn those assumptions into invariants.

Try to break those invariants.

Then verify the implementation and deployment configuration.

A smart contract should not be considered secure simply because it compiles, passes unit tests, or has no obvious static-analysis warnings.

The real question is much harder:

Can an untrusted user make the protocol enter a state that its designers never intended?

That is the question a good security review should answer.

References

  • OWASP Smart Contract Top 10 — 2025
  • Ethereum Foundation — Smart Contract Security
  • OpenZeppelin Contracts — Access Control
  • Immunefi — Crypto Losses 2024 Report
  • CertiK — Hack3d Web3 Security Report 2025

Top comments (0)