DEV Community

Benjamin
Benjamin

Posted on

Best Solidity Practices (with examples)

Here is a short list of best Solidity practices (not in any particular order). I hope this helps you!

hackerman looking at the code

1. "Don't repeat yourself" (DRY)

You probably read this a million times during your previous web2 career, but somehow you still don't apply it.
You probably read this a milliom times during your previous web2 career, but somehow you still don't apply it.

Did you notice the typo in million when looking at the text?
Your brain is a slacker unless you are committed to reading every word letter by letter, it is easy unintentionally make mistakes.

2. Don't reinvent the wheel

Relax, and spend some time researching. Read other people's code. Bookmark/star good repositories that you can reference and that can help you with your solutions.

Use battle-tested and audited code. OpenZeppelin, Solmate.

3. Use language features

Read the Solidity docs, the language has a lot of cool features.

// bad
uint256 public time = 432000;
// semi-bad (comment is helping)
uint256 public time = 432000; // 5 days
// good
uint256 public time = 5 days;

// bad
uint8 public constant MAX_LIMIT = 255;
// good
uint8 public constant MAX_LIMIT = type(uint8).max;
Enter fullscreen mode Exit fullscreen mode

4. Don't put unnecessary stuff in your smart contracts

Do you need to have it stored on the chain? If it is not necessary for your smart contract, use the event, or even better, remove it completely

Let's say that your front end wants to have a fancy title where it says "DoSomething was called {X} times."

You could add a counter to your smart contract, and have your front end do an RPC call and read it directly from the chain.
Soon enough your dapp will have a lot of RPC calls, and it will become slow + your transactions will cost more gas because you are storing and updating additional data.

// Bad
uint258 public somethingCounter;

function doSomething() external {
  somethingCounter++;
  // logic
}

// Good
event DidSomething();

function doSomething() external {
  // logic
  emit DidSomething()
}

// You don't need to have a counter and 
// store that information on the chain. 
// Use The graph protocol to index events 
// and have that information there.
Enter fullscreen mode Exit fullscreen mode

5. Use NatSpec and write comments on your code. Good example of how it should be done (bonus points for assembly)

The code should be self-documenting. But sometimes it can be really useful to add a comment that explains your taught process. Think of it as 'helping your future self'.

Bonus tip: Don't trust other people's comments. Verify yourself. Better safe than sorry, especially when you are doing some integration with another protocol.

6. Use private/internal visibility for state variables

This way you have more control. It is also a good practice to name private/internal variables with the underscore _variableName. This also helps with naming in other parts of the smart contract. (demonstrated in the example)

7. Use inheritance to your advantage

Instead of mixing logic for different things, split the logic into a different smart contract. A lot of projects have Pausable functionality that only the owner can trigger. Instead of writing that logic in your Core smart contract, it is better to write it in a separate one. Because of that, you won't see the OwnablePausable smart contract from OpenZeppelin. Instead, the logic for that concerns the owner is in Ownable, and the logic for pausing is in Pausable. Keep that in mind when designing smart contracts.

8. Security & readability > gas optimisations

Readability means more security, as there is a better chance that somebody will see the bug before it reaches production.

Example

// No Natspec
contract Something {
    address public owner;
    uint256 public multiplier = 123;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    constructor(address _owner) {
        // Repetitive code, it is easy to forget to emit an event
        owner = _owner;
        emit OwnershipTransferred(address(0), _owner);
        // ...
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Unauthorized");
        _;
    }

    // Because our owner state variable is `public owner`
    // We often see these ugly parameter names "_owner" or "owner_"
    function transferOwnership(address _owner) onlyOwner {
        // Repetitive code
        owner = _owner;
        emit OwnershipTransferred(address(0), _owner);
    }

    function doSomething() external returns(uint256) {
       uint256 _multiplier = multiplier;
       // ...
    }

    // ... other code
}
Enter fullscreen mode Exit fullscreen mode

In this simple example, you can see that the transferOwnership and constructor look almost identical.

Instead of repeating yourself, you can use an internal function that will clean up this code and make things more readable for you, your team, and your auditors.

Good code example

/**
 * @title  Something
 * @author Author name / Organization name
 * @notice Implementation of something...
 * @custom Security-contact email@email.com
 */
contract Something {
    address private _owner;
    uint256 private _multiplier = 123;

    // Events and custom errors can be moved to the interface
    // ISomething.sol 
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    constructor(address owner) {
        _transferOwnership(owner);
        // ...
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        require(msg.sender == _owner, "Unauthorized");
        _;
    }

    /**
     * @dev Does something.
     */
    function doSomething() external returns(uint256) {
       uint256 multiplier = _multiplier;

       // use multiplier from memory to save some gas 
       // (MLOAD is cheaper than SLOAD)
       // No need to use multiplierMemory, 
       // _multiplier, multiplier_, 
       // cachedMultiplier or whatever weird variable name
       // you can come up to
       // ..  
    }

    /**
     * @param owner - Address of a new owner
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address owner) onlyOwner {
        _transferOwnership(owner);
    }

    /**
     * @param owner - Address of a new owner
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address owner) internal {
        _owner = owner;
        emit OwnershipTransferred(address(0), owner);
    }
// Because the logic for transferring ownership is contained 
// in its internal function,
// you just need to review it once and make sure
// that it doesn't contain any bugs, 
// and use it whenever you need to transfer ownership. 
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Please reach out if you have any questions/suggestions.

Top comments (0)