Use Explicit Visibility Modifiers

Always specify visibility modifiers (public, private, internal, external) for functions and state variables. This improves code clarity and prevents unintended access.

// Good practice: Explicit visibility
contract Example {
    uint private secretValue;
    
    function setSecretValue(uint newValue) internal {
        secretValue = newValue;
    }
    
    function getSecretValue() external view returns (uint) {
        return secretValue;
    }
}

Omitting visibility can lead to security issues, especially in complex contracts with multiple inheritance.


Prefer view and pure Functions

Mark functions that do not modify the state as view or pure to signal intent and reduce gas costs.

// Good: Use view for read-only functions
function calculateBonus(uint amount) public view returns (uint) {
    return amount * 15 / 100;
}

// Good: Use pure for functions that don't read state
function add(uint a, uint b) public pure returns (uint) {
    return a + b;
}

Using view and pure helps the compiler optimize and alerts users that the function does not alter blockchain state.


Use require for Input Validation

Always validate inputs using require statements to prevent invalid state transitions and save gas by halting execution early.

function deposit(uint amount) external {
    require(amount > 0, "Amount must be greater than zero");
    require(balance[msg.sender] + amount >= balance[msg.sender], "Overflow detected");
    balance[msg.sender] += amount;
}

Using require ensures that invalid operations are caught and logged, improving contract robustness.


Avoid Inline Assembly Unless Necessary

Solidity provides low-level inline assembly for advanced use cases. Use it sparingly, as it can make code harder to audit and maintain.

assembly {
    let x := add(1, 2)
    mstore(0x80, x)
    return(0x80, 0x20)
}

Resort to inline assembly only when you need to access EVM features not exposed via Solidity. Always document and test such code thoroughly.


Use SafeMath for Arithmetic Operations

Even though Solidity 0.8.0 and above includes implicit overflow checks, using SafeMath from OpenZeppelin can still be beneficial for contracts targeting older versions or requiring extra safety.

import "@openzeppelin/contracts/utils/math/SafeMath.sol";

contract Calculator {
    using SafeMath for uint;

    function add(uint a, uint b) public pure returns (uint) {
        return a.add(b);
    }
}

Using SafeMath ensures that arithmetic operations do not silently overflow, which is a common source of bugs in smart contracts.


Limit Use of tx.origin

Avoid using tx.origin for access control. It can be exploited in phishing attacks and is generally unsafe in multi-hop contracts.

// Bad practice
if (msg.sender == owner || tx.origin == owner) {
    // allow action
}

// Good practice
if (msg.sender == owner) {
    // allow action
}

Always use msg.sender for access control logic.


Use Events for Debugging and Logging

Emit events to track contract activity and improve transparency. Events are also useful for off-chain monitoring and analytics.

event Deposit(address indexed user, uint amount);

function deposit(uint amount) external {
    require(amount > 0, "Amount must be greater than zero");
    balance[msg.sender] += amount;
    emit Deposit(msg.sender, amount);
}

Events can be indexed for faster querying and provide a clear audit trail.


Follow a Naming Convention

Use a consistent naming convention such as UPPER_CASE for constants and camelCase for variables and functions.

contract Token {
    string public name = "MyToken";
    uint public constant MAX_SUPPLY = 1_000_000;
    
    function transfer(address to, uint amount) public {
        // ...
    }
}

Consistent naming improves code readability and maintainability.


Avoid Excessive Use of public State Variables

Public state variables automatically create a getter function. Use private state variables with explicit getters when necessary.

// Bad practice
uint public balance;

// Good practice
uint private balance;

function getBalance() public view returns (uint) {
    return balance;
}

This gives you more control over access and allows for additional logic in the getter.


Use Reentrancy Guards

Prevent reentrancy attacks by using OpenZeppelin's ReentrancyGuard or implementing a mutex pattern.

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract Wallet is ReentrancyGuard {
    function withdraw(uint amount) external nonReentrant {
        // withdrawal logic
    }
}

Reentrancy is a common and dangerous vulnerability that can lead to fund loss.


Learn more with useful resources