Essential Security Practices for Solidity Coding and Key Security Concerns to Address

Essential Security Practices for Solidity Coding and Key Security Concerns to Address

Solidity is the programming language used to develop smart contracts on the Ethereum blockchain and on Ethereum Virtual Machine compatible layers. While Solidity offers immense possibilities for creating decentralised applications, it is essential to prioritize security when writing smart contracts. This article highlights best practices for coding safely in Solidity and highlights known security issues to watch out for.

Use the Latest Version of Solidity

Always use the latest stable version of Solidity, as it often includes important security updates and bug fixes. Regularly update your development environment to ensure compatibility with the latest Solidity compiler. The Solidity team regularly releases new versions to address security vulnerabilities and bugs identified in previous versions. By using the latest version, you can take advantage of these security updates and ensure your smart contracts are more resistant to potential attacks. As of July 2023 at the time of writing this article, the latest version was or is Version 0.8.20 have had bug fixes such as

  • SMTChecker: Fix false positives in ternary operators that contain verification targets in its branches, directly or indirectly.
  • ABI: Include events in the ABI that are emitted by a contract but defined outside of it.
  • Immutables: Disallow initialisation of immutables in try/catch statements

You can read more about solidity versions here: ethereum/solidity . As the Solidity ecosystem evolves, new features and enhancements are introduced. By using the latest version, you ensure compatibility with the latest tools, frameworks, and libraries that rely on those features. It helps you leverage the most up-to-date advancements in the Solidity language.

While it is not always mandatory to use the latest version of Solidity, it is highly recommended to stay as up-to-date as possible. However, before upgrading, thoroughly review the release notes and consider the potential impact on your existing codebase. Some updates might introduce breaking changes, requiring modifications to your contracts or dependencies. Therefore, it's crucial to test and validate your code thoroughly after any upgrades.

Ultimately, using the latest version of Solidity is a proactive approach to security and aligns with industry best practices, keeping your smart contracts better protected and benefiting from the latest advancements in the language.

Follow the Principle of Least Privilege

Adhere to the principle of least privilege by providing only the necessary access and permissions to functions, variables, and contracts. Avoid using the "public" visibility keyword unless explicitly required, and use "private" or "internal" whenever possible. Solidity provides visibility modifiers such as public, private, internal, and external to control access to functions and variables. By default, functions are set to public, which means they can be accessed by any other contract or external entity. It is generally recommended to use the most restrictive visibility modifier possible. For example, if a function or variable does not need to be accessed outside the contract, mark it as private or internal.

Example 1: Bad Code - Violating least Privilege

contract Bank {
address public owner;
uint public balance;

constructor() {
    owner = msg.sender;
}

function deposit() public payable {
    balance += msg.value;
}

function withdraw(uint amount) public {
    require(msg.sender == owner, "Only the contract owner can perform this operation.");
    require(amount <= balance, "Insufficient balance.");
    balance -= amount;
    msg.sender.transfer(amount);
}

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

This smart contract, can be used to deposit and withdraw tokens but it has some security issues. Using the public visibility modifier for the owner and balance variables, making them directly accessible outside the contract. This exposes sensitive information that should ideally be restricted.

Allowing any address to call the withdraw function, without enforcing that only the contract owner can perform this operation. This exposes a security vulnerability as anyone can withdraw funds from the contract. In this code example, by making the owner public, and not enforcing an onlyowner rule, one can set a new owner and redraw money. By not applying the principle of least privilege, this code increases the attack surface and compromises the security of the contract.

Example 2: Good Code - Applying Least Privilege

contract Bank {
address private owner; // Access restricted to contract internals 
onlyuint private balance; // Access restricted to contract internals only

constructor() {
    owner = msg.sender; // Only the deployer of the contract becomes the owner
}

modifier onlyOwner() {
    require(msg.sender == owner, "Only the contract owner can perform this operation.");
    _;
}

function deposit() public payable {
    balance += msg.value;
}

function withdraw(uint amount) public onlyOwner {
    require(amount <= balance, "Insufficient balance.");
    balance -= amount;
    msg.sender.transfer(amount);
}

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

This Smart contract is the same as the one before but by using the private visibility modifier for the owner and balance variables, ensuring they are not directly accessible outside the contract. Defining the onlyOwner modifier to restrict certain functions to be executed only by the contract owner. Applying the onlyOwner modifier to the withdraw and getBalance functions, ensuring that only the contract owner can perform these operations. By using these techniques, the contract limits access to sensitive functionality and data, providing the necessary privileges only to the contract owner.

Proper access control mechanisms are crucial to protect your smart contracts. Utilize role-based access control (RBAC) patterns, such as the Ownable or "OpenZeppelin's AccessControl" contracts, to manage contract ownership and restrict unauthorized access.

Validate external contract addresses

Validating external contract addresses is an important security practice to ensure that your Solidity code interacts with legitimate contracts. Here's an example of how you can validate an external contract address:

contract MyContract {
	function interactWithExternalContract(address externalContractAddress) public {
    require(isContract(externalContractAddress), "Invalid contract address.");   
    
    // Perform actions with the validated external contract
    // ...
}

function isContract(address addr) internal view returns (bool) {
    uint32 size;
    assembly {
        size := extcodesize(addr)
    }
    return (size > 0);
}
}

In the example above, the interactWithExternalContract function takes an externalContractAddress as a parameter. Before interacting with the contract at that address, it calls the isContract internal function to validate the address.

The isContract function uses assembly code to check the extcodesize of the given address. If the extcodesize is greater than zero, it indicates that there is code deployed at that address, confirming that it is a valid contract address. If the extcodesize is zero, it means that the address does not point to a valid contract.

By incorporating this validation step, you ensure that your Solidity code interacts only with actual contracts, reducing the risk of mistakenly interacting with external accounts or malicious addresses.

Handle Errors and Exceptions

Solidity does not have a built-in exception mechanism like some other programming languages. Instead, it relies on specific error handling mechanisms to handle exceptional conditions. The most commonly used mechanisms are require, assert, and revert statements.

The require statement is used to validate inputs and enforce preconditions. If the condition specified in the require statement evaluates to false, it immediately throws an exception, reverts all state changes, and consumes the remaining gas. It is commonly used to validate inputs and protect against invalid or malicious usage.

contract MyContract {
	function withdraw(uint amount) public {
    	require(amount > 0, "Invalid withdrawal amount."); // Validation using require    
        
        // Perform withdrawal logic
    // ...
}
}

In this example, the require statement ensures that the withdrawal amount is greater than zero. If it is not, the function throws an exception with the specified error message and reverts any state changes made prior to the require statement.

assert statement: The assert statement is used to check for conditions that should never be false. It is mainly used for internal consistency checks. If the condition specified in the assert statement evaluates to false, it throws an exception, reverts all state changes, and consumes the remaining gas.

contract MyContract {
	uint public totalSupply;
    
	function mint(uint amount) public {
    uint newTotalSupply = totalSupply + amount;
    assert(newTotalSupply > totalSupply); // Internal consistency check using 		assert

    // Perform minting logic
    // ...
}
}

In this example, the assert statement ensures that the new total supply after minting is greater than the previous total supply. If it is not, it indicates an internal error, and the function throws an exception, reverting any state changes made prior to the assert statement.

revert statement: The revert statement is used to explicitly revert state changes and provide a custom error message. It can be used when you want to handle exceptional scenarios in a specific way and communicate custom error messages.

contract MyContract {
	function divide(uint numerator, uint denominator) public returns (uint) {
    	require(denominator > 0, "Denominator should be greater than zero.");   
        if (numerator % denominator != 0) {
        revert("Numerator is not divisible by denominator.");
    }

    return numerator / denominator;
}
}

In this example, the revert statement is used to explicitly revert the state changes and provide a custom error message when the numerator is not divisible by the denominator.

By incorporating error handling mechanisms like require, assert, and revert, you can effectively handle exceptional scenarios in Solidity, validate inputs, enforce preconditions, and maintain the integrity of your smart contracts.

Be Wary of External Contracts

When coding in Solidity, it is essential to be cautious when interacting with external contracts. External contracts are contracts deployed by other individuals or organizations, and interacting with them introduces potential security risks. Here's an explanation of why you should be wary of external contracts

When interacting with an external contract, you are relying on its correctness and security. However, you cannot assume that all external contracts have undergone the same level of security audits or adhere to best coding practices. It is crucial to evaluate the reputation, code quality, and security measures of the external contract before integrating it into your own contracts. External contracts can be intentionally malicious, designed to exploit vulnerabilities in your contract or steal funds. They might employ various attack vectors, such as reentrancy attacks, where they repeatedly call back into your contract to manipulate state and extract funds. By being cautious and performing due diligence, you can minimize the risk of integrating malicious contracts into your system. When your contract relies on external contracts, you introduce dependencies. If the external contract experiences a security issue or is upgraded with breaking changes, it can affect the functionality and security of your contract. Regularly monitor the external contracts you depend on, and ensure compatibility with their latest versions. Solidity contracts are typically immutable once deployed, meaning you cannot modify their code after deployment. If an external contract you interact with undergoes an upgrade or fix, you might be unable to benefit from those changes unless you redeploy your own contract. Be aware of this limitation and carefully consider the upgradability implications when interacting with external contracts.

To mitigate the risks associated with external contracts, consider the following practices:

  1. Perform thorough audits and reviews of the external contract's code and reputation before integrating it into your own system.
  2. Use well-audited and widely-used libraries and contracts when available, as they have undergone community scrutiny and security audits.
  3. Limit the scope of interaction with external contracts and minimize the amount of sensitive data or funds exposed to them.
  4. Regularly monitor the external contracts you depend on for any updates, security advisories, or vulnerability disclosures.
  5. Maintain a flexible and upgradable contract architecture to adapt to changes in external contracts when necessary.

By exercising caution, performing due diligence, and being aware of the potential risks associated with external contracts, you can better protect your contracts and minimize the likelihood of security vulnerabilities or malicious attacks.

Copying Solidity code from the internet

Copying and pasting Solidity code from the internet can be a convenient way to leverage existing solutions or learn from code examples. However, it's important to exercise caution and follow best practices to ensure the security and reliability of your smart contracts. Before using any code snippet from the internet, take the time to thoroughly understand what the code does, including its functionality, security implications, and potential risks. Avoid blindly copying and pasting code without understanding its underlying logic and potential vulnerabilities. Ensure that the source of the code is reputable, reliable, and trusted within the Solidity community. Use code from well-known platforms, official documentation, or reputable repositories like GitHub. Review the author's reputation, code quality, and community feedback before relying on their code.

Let’s take a look at an example of a code used to flip a coin that looks harmless but contains a vulnerability that allows an attacker to predict the outcome.

contract CoinFlip {
	uint private seed;

	constructor() public {
    seed = uint(keccak256(abi.encodePacked(block.timestamp)));
}

function flip() public view returns (bool) {
    return (seed % 2 == 0);
}

function getRandomNumber() public view returns (uint) {
    return seed;
}
}

The vulnerability lies in the use of block.timestamp as the seed to generate a random number. In Solidity, block.timestamp can be manipulated by miners to some extent, which compromises the randomness of the generated number. An attacker could potentially manipulate the timestamp to influence the outcome of the coin flip and gain an unfair advantage.

If this code is blindly copied and used in a real-world scenario where fairness and unpredictability are crucial, it could result in exploitable and undesirable behavior. Instead, it is recommended to use reliable sources or external libraries for generating random numbers in a secure manner.

It is important to carefully review the code you find on the internet, understand its implications, and consider potential vulnerabilities or weaknesses before using it in your smart contracts. Always prioritize security and reliability when selecting code snippets for your own projects.

Regular Security Audits


Conduct regular security audits of your smart contracts by engaging independent third-party auditors. Audits help identify vulnerabilities, provide recommendations for improvements, and enhance the overall security posture of your contracts. This really helps as there are some things we developers can miss and that is ok. Getting feedback from others on your work can greatly help find vulnerabilities and things that you missed. You are not an island so working together and getting reviews from others is really to your advantage.

While this article covers some best practices, there are other practices you may consider by reading 6 Solidity Smart Contract Security Best Practices, Ethereum Smart Contract Best Practices and other articles on the matter. Building secure smart contracts in Solidity requires diligence and adherence to best practices. By following these guidelines, using reputable libraries, and staying updated on emerging security issues, developers can minimize the risk of vulnerabilities and protect their contracts from potential exploits. Prioritising security from the early stages of development can help foster trust, enhance the integrity of decentralised applications, and contribute to the growth of the blockchain ecosystem as a whole.

Connect with Bitfinity Network

Bitfinity Wallet | Bitfinity Network | Twitter | Telegram | Discord | Github

*Disclaimer: While every effort is made on this website to provide accurate information, any opinions expressed or information disseminated do not necessarily reflect the views of Bitfinity itself.