Solidity Security Tips
Write secure Solidity smart contracts. This guide covers Solidity-specific security patterns and best practices to prevent common vulnerabilities. --- solidity...
Last updated: January 14, 2026
Solidity Security Tips
Write secure Solidity smart contracts.
Overview
This guide covers Solidity-specific security patterns and best practices to prevent common vulnerabilities.
Compiler Settings
Use Latest Stable Version
// Good: Specific recent version
pragma solidity 0.8.20;
// Avoid: Floating pragma
pragma solidity ^0.8.0;
// Avoid: Old versions
pragma solidity 0.6.12;
Optimizer Settings
# foundry.toml
[profile.default]
optimizer = true
optimizer_runs = 200 # Balance deployment vs. runtime cost
[profile.production]
optimizer = true
optimizer_runs = 10000 # More runtime-optimized
Access Control
Use OpenZeppelin
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
// Simple ownership
contract Token is Ownable {
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
}
// Role-based access
contract Vault is AccessControl {
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
function withdrawFees() public onlyRole(ADMIN_ROLE) {
// ...
}
function pauseVault() public onlyRole(OPERATOR_ROLE) {
// ...
}
}
Two-Step Ownership Transfer
import "@openzeppelin/contracts/access/Ownable2Step.sol";
contract SafeOwnable is Ownable2Step {
// transferOwnership() requires acceptOwnership() from new owner
}
Reentrancy Protection
Use ReentrancyGuard
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract Vault is ReentrancyGuard {
function withdraw(uint256 amount) public nonReentrant {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}
}
Checks-Effects-Interactions Pattern
function withdraw(uint256 amount) public {
// 1. Checks
require(balances[msg.sender] >= amount, "Insufficient balance");
require(amount > 0, "Amount must be positive");
// 2. Effects (state changes)
balances[msg.sender] -= amount;
totalDeposits -= amount;
// 3. Interactions (external calls)
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
emit Withdrawal(msg.sender, amount);
}
Safe External Calls
SafeERC20 for Token Transfers
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract TokenHandler {
using SafeERC20 for IERC20;
function deposit(IERC20 token, uint256 amount) public {
// Handles non-standard tokens (USDT, etc.)
token.safeTransferFrom(msg.sender, address(this), amount);
}
function withdraw(IERC20 token, address to, uint256 amount) public {
token.safeTransfer(to, amount);
}
}
Check Low-Level Call Results
function sendEther(address payable to, uint256 amount) public {
// BAD
to.call{value: amount}("");
// GOOD
(bool success, ) = to.call{value: amount}("");
require(success, "Transfer failed");
}
Input Validation
Validate All Parameters
function setConfig(
address token,
uint256 fee,
uint256 maxAmount
) public onlyOwner {
require(token != address(0), "Invalid token");
require(fee <= MAX_FEE, "Fee too high");
require(maxAmount >= MIN_AMOUNT, "Amount too low");
require(maxAmount <= MAX_AMOUNT, "Amount too high");
config.token = token;
config.fee = fee;
config.maxAmount = maxAmount;
}
Use Custom Errors (Gas Efficient)
error InvalidAddress(address provided);
error FeeTooHigh(uint256 provided, uint256 maximum);
error AmountOutOfRange(uint256 provided, uint256 min, uint256 max);
function setFee(uint256 newFee) public onlyOwner {
if (newFee > MAX_FEE) {
revert FeeTooHigh(newFee, MAX_FEE);
}
fee = newFee;
}
Arithmetic Safety
Solidity 0.8+ Built-in Protection
// Automatically reverts on overflow/underflow
uint256 a = type(uint256).max;
uint256 b = a + 1; // Reverts
// Use unchecked only when overflow is impossible
function sumArray(uint256[] memory arr) public pure returns (uint256) {
uint256 sum;
for (uint256 i; i < arr.length; ) {
sum += arr[i];
unchecked { ++i; } // Loop counter can't overflow
}
return sum;
}
Division Precision
// BAD: Precision loss
uint256 share = amount / totalSupply * userBalance;
// GOOD: Multiply before divide
uint256 share = amount * userBalance / totalSupply;
// BEST: Use higher precision
uint256 PRECISION = 1e18;
uint256 share = amount * userBalance * PRECISION / totalSupply / PRECISION;
Storage Patterns
Initialize Properly
contract Initializable {
bool private _initialized;
modifier initializer() {
require(!_initialized, "Already initialized");
_initialized = true;
_;
}
function initialize(address admin) public initializer {
_admin = admin;
}
}
Use Immutable for Constants
contract Token {
// Gas efficient - stored in bytecode
address public immutable owner;
uint256 public immutable deployTime;
IERC20 public immutable baseToken;
constructor(IERC20 _baseToken) {
owner = msg.sender;
deployTime = block.timestamp;
baseToken = _baseToken;
}
}
Event Emission
Log All State Changes
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
event FeeUpdated(uint256 oldFee, uint256 newFee);
event Paused(address account);
event Unpaused(address account);
function setOwner(address newOwner) public onlyOwner {
emit OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
Use Indexed Parameters
event Transfer(
address indexed from, // Indexed for filtering
address indexed to, // Indexed for filtering
uint256 amount // Not indexed - stored in data
);
Gas Optimization Without Sacrificing Security
Safe Optimizations
// Cache array length
function process(uint256[] memory items) public {
uint256 length = items.length; // Cache
for (uint256 i; i < length; ) {
// Process items[i]
unchecked { ++i; } // Safe: i < length
}
}
// Use calldata for read-only arrays
function sum(uint256[] calldata numbers) public pure returns (uint256) {
// calldata is cheaper than memory
}
// Pack structs
struct User {
address wallet; // 20 bytes
uint64 balance; // 8 bytes
uint32 lastUpdate; // 4 bytes
// Total: 32 bytes (1 slot)
}
Avoid Unsafe Optimizations
// DON'T skip checks for gas savings
function unsafeTransfer(address to, uint256 amount) public {
// Skipping balance check is dangerous!
balances[msg.sender] -= amount;
balances[to] += amount;
}
// DO include safety checks
function safeTransfer(address to, uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient");
require(to != address(0), "Invalid recipient");
balances[msg.sender] -= amount;
balances[to] += amount;
}
Common Pitfalls
tx.origin vs msg.sender
// BAD: tx.origin can be manipulated
function withdraw() public {
require(tx.origin == owner); // Vulnerable to phishing
}
// GOOD: Use msg.sender
function withdraw() public {
require(msg.sender == owner);
}
Delegatecall Dangers
// BAD: User-controlled delegatecall
function execute(address target) public {
target.delegatecall(msg.data);
}
// GOOD: Controlled implementation
address public implementation;
function upgrade(address newImpl) public onlyOwner {
implementation = newImpl;
}
Approve Race Condition
// Use increaseAllowance/decreaseAllowance instead of approve
function safeIncreaseAllowance(
IERC20 token,
address spender,
uint256 addedValue
) internal {
uint256 currentAllowance = token.allowance(address(this), spender);
token.approve(spender, currentAllowance + addedValue);
}
Testing Recommendations
Test Security Properties
function test_ReentrancyProtection() public {
ReentrantAttacker attacker = new ReentrantAttacker(vault);
vm.expectRevert();
attacker.attack();
}
function test_AccessControlEnforced() public {
vm.prank(nonOwner);
vm.expectRevert("Ownable: caller is not the owner");
vault.withdrawFees();
}
function testFuzz_NoOverflow(uint256 a, uint256 b) public {
vm.assume(a <= type(uint128).max);
vm.assume(b <= type(uint128).max);
assertEq(calculator.add(a, b), a + b);
}
Next Steps
- Vyper Security Tips - Vyper patterns
- Common Vulnerability Patterns - Known issues
- Solidity Language Guide - Full guide