Skip to content

Instantly share code, notes, and snippets.

@Favourokerri
Created January 31, 2025 11:51
Show Gist options
  • Save Favourokerri/efdeec553c08e6c1c2052d5c8ab16c1a to your computer and use it in GitHub Desktop.
Save Favourokerri/efdeec553c08e6c1c2052d5c8ab16c1a to your computer and use it in GitHub Desktop.

Solidity Best Practices: Writing Efficient and Secure Smart Contracts

Solidity is the backbone of Ethereum smart contracts, and writing efficient, secure, and gas-optimized code is crucial. In this guide, we'll explore best practices to help you develop better Solidity contracts.


1. Use Proper Naming Conventions

Naming conventions improve code readability and maintainability. In Solidity:

  • Immutable variables should use an i_ prefix (e.g., i_price).
  • Constant variables should be written in uppercase with underscores (e.g., MAX_SUPPLY).
uint256 immutable i_price;
uint256 constant MAX_SUPPLY = 1000;

This convention makes it clear which variables will not change after deployment.


2. Use constant and immutable to Reduce Gas Costs

If a variable doesn't need to be modified after deployment, declare it as constant or immutable. These variables are stored in bytecode instead of storage, reducing gas costs.

uint256 constant TOKEN_SUPPLY = 1_000_000;
address immutable i_owner;

constructor() {
    i_owner = msg.sender; // Immutable variable set in constructor
}

3. Use private Variables with Getter Functions

Not all variables should be publicly accessible. Marking them as private improves security, and you can create custom getter functions when needed.

contract Example {
    uint256 private s_balance;

    function getBalance() external view returns (uint256) {
        return s_balance;
    }
}

This prevents direct access while still allowing controlled retrieval of data.


4. Use external for Functions Called Externally

Functions not used within the contract should be marked as external to save gas.

contract Example {
    function externalFunction() external view returns (uint256) {
        return 42;
    }
}

external functions use less gas compared to public functions.


5. Follow Solidity Style Guide for Function Ordering

Maintaining a consistent function order improves readability. Follow this sequence:

  1. Constructor
  2. Receive & fallback functions
  3. External functions
  4. Public functions
  5. Internal functions
  6. Private functions

This ensures a clean and structured codebase.


6. Use Custom Errors Instead of require Strings

Using require with strings is expensive because storing strings in transaction logs consumes more gas. Instead, use custom errors:

error NotOwner();

contract Example {
    address private immutable i_owner;

    constructor() {
        i_owner = msg.sender;
    }

    function onlyOwner() external {
        if (msg.sender != i_owner) revert NotOwner();
    }
}

Custom errors save gas by reducing storage costs.


7. Make Arrays payable When Storing Addresses for ETH Transfers

If your contract stores addresses that will receive ETH, declare them as payable:

address payable[] private s_players;

This ensures that ETH transfers can be made directly to these addresses.


8. Emit Events When Changing State Variables

Whenever a state variable changes, emit an event. This helps in tracking contract changes, especially for migration purposes.

event WinnerSelected(address indexed winner);

function pickWinner(address winner) external {
    s_recentWinner = winner;
    emit WinnerSelected(winner);
}

Events improve transparency and make debugging easier.


9. Use Chainlink VRF for Random Number Generation

Ethereum is deterministic, meaning smart contracts can't generate true randomness. Use Chainlink VRF for secure random numbers:

import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";

Chainlink VRF ensures provable randomness, making it ideal for lotteries and games.


10. Add Parent Constructors When Inheriting

If a contract inherits from another contract with a constructor, ensure you initialize it in your derived contract’s constructor:

contract Parent {
    uint256 public value;
    constructor(uint256 _value) {
        value = _value;
    }
}

contract Child is Parent {
    constructor(uint256 _value) Parent(_value) {}
}

Neglecting this may cause compilation errors.


11. Use enum to Track Contract States

enum is a useful way to manage contract states:

enum LotteryState { Open, Closed, Calculating }
LotteryState public s_lotteryState;

This improves code clarity and helps prevent invalid states.


12. Follow the Checks-Effects-Interactions (CEI) Pattern

To prevent reentrancy attacks, use the Checks-Effects-Interactions pattern:

  1. Checks – Validate conditions first.
  2. Effects – Update contract state.
  3. Interactions – Interact with external contracts last.
function withdrawFunds() external {
    require(msg.sender == i_owner, "Not owner"); // Check
    uint256 amount = address(this).balance;
    s_balance = 0; // Effect (state update)
    (bool success, ) = msg.sender.call{value: amount}(""); // Interaction
    require(success, "Transfer failed");
}

Following CEI reduces security risks.


Conclusion

By following these Solidity best practices, you can create secure, gas-efficient, and well-structured smart contracts. Whether you're building a lottery system, a DeFi protocol, or an NFT marketplace, these guidelines will help ensure your contracts are robust and optimized.

🔹 Key Takeaways: ✅ Use constant and immutable for gas savings. ✅ Prefer private variables with getter functions. ✅ Mark functions external when not used internally. ✅ Use custom errors instead of require strings. ✅ Emit events when changing state variables. ✅ Always follow the Checks-Effects-Interactions (CEI) pattern.

By integrating these best practices, you can write better Solidity contracts and improve overall blockchain security. 🚀


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment