Smart contracts power decentralized applications (dApps), DeFi protocols, and blockchain-based systems. However, vulnerabilities in Solidity code can lead to catastrophic losses—millions have been stolen due to simple oversights.
To help you write secure, gas-efficient, and production-ready smart contracts, here are 10 essential Solidity best practices every developer should follow.
1. Use the Latest Solidity Version
Always compile with the most recent stable version of Solidity (e.g., 0.8.x). Newer versions include security fixes, gas optimizations, and improved syntax.
pragma solidity ^0.8.0; // Use the latest stable version
Why? Older versions (e.g., 0.4.x or 0.6.x) may contain unchecked math operations or other vulnerabilities.
2. Enable Compiler Warnings & Use Static Analysis Tools
Configure solc to treat warnings as errors:
{
"compilerOptions": {
"strict": true
}
}
Additionally, use tools like:
- Slither (static analysis)
- MythX (security analysis)
- Foundry's
forge inspect(gas reports)
3. Follow the Checks-Effects-Interactions Pattern
Avoid reentrancy attacks by:
- Checks: Validate conditions (e.g., balances, permissions).
- Effects: Update contract state.
- Interactions: Call external contracts.
function withdraw(uint amount) external {
require(balances[msg.sender] >= amount); // Check
balances[msg.sender] -= amount; // Effect
(bool success, ) = msg.sender.call{value: amount}(""); // Interaction
require(success);
}
Calling external contracts before state changes can lead to reentrancy.
4. Use require(), assert(), and revert() Properly
require(condition, "error")– Validate inputs & conditions (refunds gas).assert(condition)– Check invariants (consumes all gas; use for internal errors).revert("error")– Explicitly revert with a message.
5. Avoid tx.origin for Authorization
if (tx.origin == owner) { ... } // Vulnerable to phishing
if (msg.sender == owner) { ... } // Use msg.sender instead
tx.origin can be manipulated by malicious contracts in a call chain.
6. Use OpenZeppelin Contracts for Security
Leverage battle-tested libraries like:
Ownable– For access control.ReentrancyGuard– Prevents reentrancy.SafeMath(in <0.8.0) – Prevents overflows.
Example:
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyContract is Ownable {
function adminAction() external onlyOwner { ... }
}
7. Optimize Gas with Fixed-Size Types & Packing
Use uint256 over uint8 (EVM operates in 256-bit slots). Pack variables tightly:
struct GasOptimized {
uint128 a; // Packed with b in same slot
uint128 b;
uint256 c; // New storage slot
}
8. Limit External Calls & Use Pull Over Push
Push payments (sending ETH directly) can fail and lock funds. Instead, use pull payments:
mapping(address => uint) public pendingWithdrawals;
function withdraw() external {
uint amount = pendingWithdrawals[msg.sender];
pendingWithdrawals[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}
9. Protect Against Frontrunning
- Use commit-reveal schemes for sensitive actions.
- Avoid revealing inputs in transactions (e.g., in auctions).
10. Write & Run Tests
Use Foundry, Hardhat, or Truffle to test:
- Unit tests (individual functions).
- Integration tests (contract interactions).
- Fuzz tests (randomized inputs).
Example Foundry test:
function testWithdrawRevertsIfInsufficientBalance() public {
vm.prank(user);
vm.expectRevert("Insufficient balance");
myContract.withdraw(100 ether);
}
Final Thoughts
Writing secure Solidity code requires discipline. Always:
- Audit your contracts.
- Follow best practices.
- Assume external calls are malicious.
Need a security review? Consider OpenZeppelin Defender or Certora for formal verification.