Smart Contract Security Checklist for Web3 Developers | PandaSecurity
Web3 Security

Smart Contract Security Checklist for Web3 Developers

PandaSecurity Team •
#Smart Contract Security#Web3 Security#Solidity
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:

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);
}

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);
}

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

You also need to understand:

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:

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

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

Check:

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

For example:

function upgradeTo(address newImplementation)
    external
{
    implementation = newImplementation;
}

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;
}

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;
}

But developers should still verify the complete initialization flow.

Check:

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;
}

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);
}

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:

A contract does not need to explicitly call:

msg.sender.call(...)

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?

For example:

token.transfer(user, amount);

is not necessarily equivalent to:

SafeERC20.safeTransfer(token, user, amount);

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:

Consider:

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

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

uint256 received = amount;

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;

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:

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

For example:

uint256 price =
    reserve1 * 1e18 / reserve0;

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:


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:

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;

may look mathematically correct.

But what happens when:

totalAssets = 0

or:

totalShares = 0

or:

assets * totalShares

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

or:

Shares minted <= proportional contribution

or:

Debt cannot become negative

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()

Then compare the accounting between these functions.

Important questions include:

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

deposit
→ donate
→ deposit
→ withdraw
→ redeem

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);

depending on the application.

Look for:

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?

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

Important parameters often include:

chainId
verifyingContract
nonce
deadline

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

Also review:

Do not assume:

ecrecover(...)

alone provides a complete signature-security model.


13. Be Careful With tx.origin

Avoid using:

require(tx.origin == owner);

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);

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++) {
    ...
}

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

Review:

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

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

when exact internal accounting is required.

Instead, distinguish between:

Protocol accounting

and:

Actual blockchain balance

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

Do not blindly copy an old contract and change:

pragma solidity ...

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

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

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

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

or:

totalSupply cannot decrease unexpectedly

or:

only authorized addresses can modify protocol parameters

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 .

can identify issues involving:

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

Static analysis is much better at finding:

known code patterns

than:

incorrect business logic

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()

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:


21. Review Deployment Configuration

A secure contract can still be deployed insecurely.

Before mainnet deployment, verify:

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

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

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

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

Access Control

DeFi Logic

Token Handling

External Calls

Upgradeability

Testing

Deployment


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."

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

Related articles

← Back to Blog