Solidity Code

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:

  1. Checks: Validate conditions (e.g., balances, permissions).
  2. Effects: Update contract state.
  3. Interactions: Call external contracts.
✅ Good:
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);
}
❌ Bad:

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

❌ Dangerous:
if (tx.origin == owner) { ... } // Vulnerable to phishing
✅ Secure Alternative:
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.