Smart Contract Security Audit Checklist: What We Look For
Contract security audit checklist: Before your Solana or EVM contract goes to mainnet, use this comprehensive audit checklist. Covers reentrancy, access cont...
Founder, WRKSHP.DEV
Contract security audit checklist: Before your Solana or EVM contract goes to mainnet, use this comprehensive audit checklist. Covers reentrancy, access cont... This guide explains Smart Contract Security Audit Checklist: What We Look For with practical patterns WRKSHP uses on client builds. Use the checklist below to prioritize next steps and avoid common mistakes that waste time and.
Before your Solana or EVM contract goes to mainnet, use this comprehensive audit checklist. Covers reentrancy, access control, economic attacks, and the subtle bugs that drain protocols. This guide explains Smart Contract Security Audit Checklist: What We Look For with practical patterns WRKSHP uses on client builds.
Contract security audit checklist overview
Teams implementing contract security audit checklist need clear ownership, observability, and rollout guardrails. The sections below walk through patterns WRKSHP uses on client builds.
Smart contract exploits have drained billions of dollars from DeFi protocols. The Ronin bridge lost $625 million. Wormhole lost $325 million. Nomad lost $190 million. Behind each exploit is a bug that seemed innocuous during development but proved catastrophic in production.
Security audits exist to find these bugs before attackers do. But audits aren't magic, they're systematic reviews following proven methodologies. This guide shares our internal audit checklist, the same framework we use when reviewing client contracts. Whether you're preparing for an external audit or conducting internal review, this checklist helps ensure nothing critical is missed.
Before You Start: Audit Preparation
A productive audit requires preparation. Auditors need context to evaluate security effectively.
Documentation Requirements
Architecture overview: How do the contracts interact? What are the trust assumptions? A diagram showing contract relationships and external dependencies is essential.
Specification: What is the contract supposed to do? Without a specification, auditors can only find bugs in implementation, not logic errors where the implementation doesn't match intent.
Known risks: What trade-offs did you make? What risks do you accept? Being explicit about known limitations prevents wasted time rediscovering intentional decisions.
Test suite: Comprehensive tests demonstrate that basic functionality works and make it easier for auditors to understand intended behavior.
Previous audits: If you've had prior audits, share the reports and how you addressed findings.
Access Control Vulnerabilities
Access control defines who can do what. Errors here typically enable unauthorized actions with catastrophic consequences.
Checklist: Access Control
1. Are admin functions properly protected?
Every function that modifies critical state should have explicit access control. "Admin-only" functions without checks are common vulnerabilities.
// Vulnerable: Anyone can pause
function pause() external {
_paused = true;
}
// Secure: Only owner can pause
function pause() external onlyOwner {
_paused = true;
}2. Is the ownership transfer process secure?
One-step ownership transfer is risky, a typo in the new owner address permanently locks admin access. Use two-step transfer requiring the new owner to accept.
// Two-step ownership transfer
address public pendingOwner;
function transferOwnership(address newOwner) external onlyOwner {
pendingOwner = newOwner;
}
function acceptOwnership() external {
require(msg.sender == pendingOwner, "Not pending owner");
owner = pendingOwner;
pendingOwner = address(0);
}3. Are role assignments validated?
Can roles be assigned to the zero address? Can critical roles be accidentally removed? Validate role assignments to prevent lockouts.
4. Is there separation of concerns?
A single compromised admin key shouldn't be able to drain all funds immediately. Consider timelocks, multi-sig requirements, or role separation for critical operations.
Input Validation
Smart contracts should never trust input. Every external call provides potentially malicious data.
Checklist: Input Validation
1. Are all function parameters validated?
Check for:
- Zero addresses where addresses shouldn't be zero
- Zero amounts where amounts should be positive
- Array lengths that could cause out-of-gas or exceed limits
- Values outside expected ranges
// Validate inputs explicitly
function deposit(address token, uint256 amount) external {
require(token != address(0), "Invalid token");
require(amount > 0, "Amount must be positive");
require(amount <= MAX_DEPOSIT, "Exceeds maximum");
// ...
}2. Are array inputs bounded?
Unbounded loops over user-provided arrays can cause denial of service through gas exhaustion.
// Vulnerable: Unbounded loop
function batchTransfer(address[] calldata recipients, uint256[] calldata amounts) external {
for (uint i = 0; i < recipients.length; i++) {
// Could run out of gas with large arrays
transfer(recipients[i], amounts[i]);
}
}
// Secure: Bounded loop
function batchTransfer(address[] calldata recipients, uint256[] calldata amounts) external {
require(recipients.length <= 100, "Too many recipients");
require(recipients.length == amounts.length, "Length mismatch");
// ...
}3. Is external data validated?
Data from oracles, other contracts, or callbacks should be validated. Don't assume external data is correct or fresh.
Reentrancy Attacks
Reentrancy occurs when an external call allows an attacker to re-enter a function before the first execution completes. The DAO hack that split Ethereum exploited reentrancy.
Checklist: Reentrancy
1. Are state changes made before external calls?
The checks-effects-interactions pattern prevents most reentrancy: check conditions, update state, then make external calls.
// Vulnerable: State updated after external call
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount);
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] -= amount; // Attacker can re-enter before this executes
}
// Secure: State updated before external call
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount; // State updated first
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}2. Are reentrancy guards used where appropriate?
For complex functions where checks-effects-interactions is difficult to verify, use explicit reentrancy guards.
uint256 private _locked = 1;
modifier nonReentrant() {
require(_locked == 1, "Reentrant call");
_locked = 2;
_;
_locked = 1;
}
function complexOperation() external nonReentrant {
// Safe from reentrancy
}3. Is read-only reentrancy considered?
Even view functions can be exploited if they read state that's temporarily inconsistent during a transaction. This is subtle and often missed.
Economic and Logic Attacks
Beyond technical bugs, smart contracts can have economic vulnerabilities where the logic is technically correct but can be exploited for profit.
Checklist: Economic Security
1. Are flash loans considered?
Attackers can borrow enormous amounts with no collateral for a single transaction. Any logic that can be manipulated with temporary large positions is vulnerable.
// Vulnerable: Can be manipulated with flash loan
function getTokenPrice() public view returns (uint256) {
// Price based on current pool ratio - can be manipulated
return reserve0 / reserve1;
}
// More secure: Use TWAP or external oracle
function getTokenPrice() public view returns (uint256) {
return oracle.getTimeWeightedPrice(token, 1 hours);
}2. Are oracle manipulations possible?
If your contract relies on price oracles, consider:
- Can the oracle be manipulated within a transaction?
- What happens if the oracle returns stale data?
- What if the oracle returns zero or extreme values?
3. Are front-running risks mitigated?
Public transactions can be observed and front-run. Consider commit-reveal schemes or private mempools for sensitive operations.
4. Is slippage protection implemented?
Any swap or trade should have minimum output requirements to prevent sandwich attacks.
// Always include slippage protection
function swap(
address tokenIn,
uint256 amountIn,
uint256 minAmountOut // User-specified minimum
) external returns (uint256 amountOut) {
amountOut = _executeSwap(tokenIn, amountIn);
require(amountOut >= minAmountOut, "Slippage exceeded");
}Integer Handling
Arithmetic bugs have caused numerous exploits. Solidity 0.8+ has built-in overflow protection, but edge cases remain.
Checklist: Integer Safety
1. Is integer overflow/underflow possible?
Solidity 0.8+ reverts on overflow by default, but unchecked blocks and older versions don't. Verify all arithmetic.
2. Is division handled correctly?
Integer division truncates. Is this handled appropriately? Consider precision loss accumulation over multiple operations.
// Loss of precision
uint256 share = (userDeposit * 1e18) / totalDeposits; // Better: Scale first
uint256 payout = (share * rewards) / 1e18;
// Be careful with order of operations
uint256 fee = amount * feePercent / 100; // OK
uint256 fee = amount / 100 * feePercent; // Precision loss if amount < 1003. Are type conversions safe?
Casting between integer sizes can truncate. Casting signed to unsigned can produce unexpected values.
External Interactions
Calls to external contracts are inherently risky. The external contract might be malicious, might fail, or might behave unexpectedly.
Checklist: External Calls
1. Are return values checked?
Some tokens don't return a boolean from transfer/transferFrom. Use SafeERC20 or check return values explicitly.
// Vulnerable: Doesn't check return value
IERC20(token).transfer(recipient, amount);
// Secure: Uses SafeERC20
using SafeERC20 for IERC20;
IERC20(token).safeTransfer(recipient, amount);2. Are fee-on-transfer tokens handled?
Some tokens take a fee on transfer. If you assume received amount equals sent amount, accounting breaks.
// Handle fee-on-transfer tokens
uint256 balanceBefore = token.balanceOf(address(this));
token.safeTransferFrom(msg.sender, address(this), amount);
uint256 received = token.balanceOf(address(this)) - balanceBefore;3. Are rebasing tokens considered?
Some tokens change balances without transfers (rebasing). If you cache balances, they may become stale.
4. What happens if an external call fails?
Consider: Should the transaction revert? Should it continue with partial success? Is there cleanup needed?
Solana-Specific Considerations
Solana's programming model differs significantly from EVM. Different vulnerability patterns emerge.
Checklist: Solana Programs
1. Are all accounts validated?
Solana programs receive accounts as parameters. Every account must be validated: is it the expected type? Is it owned by the expected program?
// Always validate account ownership
if pool.owner != program_id {
return Err(ProgramError::IncorrectProgramId);
}
// Validate expected account types
if *token_account.owner != spl_token::id() {
return Err(ProgramError::InvalidAccountData);
}2. Are signers verified?
Just because an account is passed doesn't mean the transaction was signed by it. Verify is_signer for authority accounts.
3. Are PDAs derived correctly?
Program Derived Addresses must be derived with the correct seeds. Incorrect derivation can allow account substitution attacks.
// Always verify PDA derivation
let (expected_pda, bump) = Pubkey::find_program_address(
&[b"vault", user.key.as_ref()],
program_id
);
if vault.key != &expected_pda {
return Err(ProgramError::InvalidSeeds);
}4. Is arithmetic overflow protected?
Solana/Rust doesn't overflow in debug builds but does in release builds. Use checked_add, checked_sub, etc.
// Safe arithmetic
let new_balance = balance
.checked_add(amount)
.ok_or(ProgramError::Overflow)?;Testing Requirements
Tests don't prove absence of bugs, but their absence suggests bugs are present.
Checklist: Testing
1. Are edge cases tested?
- Zero values
- Maximum values
- Empty arrays
- First and last user
- Boundary conditions
2. Are failure modes tested?
Tests should verify that unauthorized actions fail, invalid inputs are rejected, and invariants hold.
3. Is there fuzzing?
Fuzzing discovers edge cases that manual testing misses. Tools like Foundry's fuzzer or Echidna are invaluable.
4. Are economic invariants tested?
After any operation, do fundamental invariants hold? Total supply unchanged? Accounting balanced? No value created from nothing?
Conclusion
Security audits are not guarantees, they're risk reduction. Even thoroughly audited contracts can have vulnerabilities. But systematic review following proven checklists dramatically reduces risk.
Key principles:
- Assume all input is malicious
- Validate everything, trust nothing
- Update state before external calls
- Consider economic attacks, not just technical bugs
- Test edge cases and failure modes
- Get multiple sets of eyes on critical code
The cost of a security audit is trivial compared to the cost of an exploit. Invest in security before you deploy.
Ready to implement these patterns? Explore enterprise software builds and Growth OS with WRKSHP, or start a conversation.
Work With WRKSHP
WRKSHP builds AI-native software, operated growth systems, and governance layers for teams that sell outcomes, not billable hours.
Explore enterprise software, Growth OS, GaaS governance, contact, or start a conversation about your next build.
Frequently Asked Questions
What is the main takeaway from "Smart Contract Security Audit Checklist: What We Look For"?
Contract security audit checklist: Before your Solana or EVM contract goes to mainnet, use this comprehensive audit checklist. Use it as a production playbook for operators and engineers, not slide-deck theory.
How does "Contract security audit checklist overview" fit into Smart Contract Security Audit Checklist: What We Look For?
Contract security audit checklist overview is a core section of this guide. Apply it after you pick one measurable KPI, then instrument the path that moves that KPI before expanding scope.
What should I watch when working through "Before You Start: Audit Preparation"?
Treat "Before You Start: Audit Preparation" as a decision checkpoint: name an owner, define success metrics, and refuse to automate steps that spend money or change production data without an audit trail.
When should I bring in a partner on Smart Contract Security Audit Checklist: What We Look For?
Bring in help when you need a fixed-outcome delivery model, governance for agent actions, or a single platform spanning build and growth, patterns WRKSHP uses on enterprise software and Growth OS engagements.