Security Considerations While Developing Ethereum Smart Contracts in Solidity

By Erin Godanci on ALTCOIN MAGAZINE

Introduction

Smart contract security is a big issue that smart contracts face. Smart contracts ensure the security of the transactions are free from the risk of ambiguous interpretation of the conditions, as they are based on cryptography, but there are still problems that can’t be solved with smart contracts. To develop smart contracts is certainly not a free picnic. A bug introduced in the smart contract code can cost money and most likely not only your money but also other people’s as well. The reality is that the Ethereum ecosystem is still in its infancy but growing and standards are continuously being defined and redefined by the day so you need to always be updated about smart contract security features. This article provides a baseline knowledge of security considerations for intermediate Solidity developers.

1. Compiler Version Considerations

When developing smart contracts it is always recommended to use a recent version of the Solidity compiler. Using an outdated version of the compiler on development can be problematic especially if there are publicly disclosed bugs and issues that affect the current compiler version. Make sure to always check for compiler warnings as they can flag the issue within a single contract.

Usage of an outdated compiler example:

pragma solidity 0.4.0; //outdated compiler version
contract OutdatedCompiler{
uint x = 1;
}

Usage of an up to date compile example:

pragma solidity 0.4.26; // updated compiler version
contract UpdatedCompiler{
uint x = 1;
}

Contracts should always be deployed with the same compiler version and flags that they have been tested with thoroughly. Locking the pragma helps to ensure that contracts do not accidentally get deployed using, for example, an outdated compiler version that might introduce bugs that affect the contract system negatively. On smart contracts make sure to always lock the pragma version and also consider known bugs for the compiler version that is chosen.

pragma solidity ^0.4.0; //unlocked compiler 
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
pragma solidity 0.4.25; //locked compiler

2. Overflow and underflow

An overflow and underflow happen when an arithmetic operation reaches the maximum or minimum size of a type. For instance, if a number is stored in the uint8 type, it means that the number is stored in an 8 bits unsigned number ranging from 0 to 2^8–1. In computer programming an integer overflow/underflow occurs when an arithmetic operation attempts to create a numeric value that is outside of the range that can be represented with a given number of bits — either larger than the maximum or lower than the minimum representable value. It is recommended to use vetted safe math libraries such as “SafeMath by OpenZeppelin” library for arithmetic operations. consistently throughout the smart contract system.

Overflow example: Let’s say that in our contract he has a number type uint8, which can only have 8 bits. That means the largest number this can store is binary 11111111 (2^8–1=255).

Let’s take a look at the code below:

uint8 number = 255;
number++;

In this example, we just caused an Overflow, so the number is now equal to 0. If we add 1 to binary 11111111, it will reset back to 00000000. So the number is now equal to 0.

Underflow example: Let’s say that in our contract we have a number type uint8 with the assigned value of 0 and we try to subtract this value by 1. What will happen?.

Let’s take a look at the code below:

uint8 number = 0;
number — ;

In this example we just caused an underflow, so number is now equal to 255. If you subtract 1 to binary 00000000, the value will be set back to its maximum possible value which is 255 ( 2^8–1=255).

As dangerous as both cases are, the underflow case is the more likely to happen, for example in the case where a token holder has X tokens but attempts to spend X+1. If the code does not check for it, the attacker might end up being allowed to spend more tokens than he had and have a maxed out balance value.

3. Function Visibility

In Solidity, functions can be specified as being external, public, internal or private.

  • external functions: As a part of the contract interface, external functions can be called from other contracts and by transactions. Functions with external visibility cannot be accessed internally by other functions of the contract but only externally.
  • public functions: function that are public, can be called both by message calls and internally. In the case of public state variables, they have automatic getter function generated for them.
  • private functions: contract functions and state variables declared as private will only be visible within the contract that they have been declared on, and can not be accessed by any derived contract.
  • internal functions: internal functions and state variables are only accessible internally from the contract that they are in, without using this and it allows contracts that inherit from the parent contract to use that function.

Tips :

  • Make sure to always keep your functions private or internal unless there is a need for outside interaction.
  • External is cheaper to use because it uses the call data opcode while public needs to copy all the arguments to memory.

While developing smart contracts in Solidity it is recommended to make a conscious decision on which visibility type is appropriate for that particular function. This can highly reduce the attack surface of a contract system. Choosing an inappropriate visibility for a function or a state variable can lead to a vulnerability if a developer did not set the appropriate visibility type and a malicious user then is able to make unauthorized or unintended state changes.

Let’s take a look at the vulnerable code below :

function withdrawFunds() public {
require(msg.sender == owner);
transferFunds();
}
function transferFunds() public { // vulnerable function declared as public.
msg.sender.transfer(balance);
}

Function transferFunds() visibility is set as public meaning that it can be called internally and by message calls. Any malicious user is able to call this function and transfer balance to its own address just by calling this function even if he’s not the owner. In this case we need to use internal function visibility so the function can only be called internally.

function transferFunds() internal { // function declared as internal.
msg.sender.transfer(balance);
}

4. Unchecked return values from function calls

When the return value of a message call is not checked, execution will resume even if the called contract throws an exception. If the call fails accidentally or an attacker forces the call to fail, this may cause unexpected behavior in the subsequent program logic. Always make sure to handle the possibility that the call will fail by checking the return value of that function.

Let’s take a look at the example code below:

address contractB = 0x4c9D562226ad4fb5EEA1596e92c02A146BE01693
function callNotChecked() public {
contractB.call(); // vulnerable
}

The code example above show us that the call() function for the contractB is not checked and if an unexpected error occurs that error is not handled, the execution will resume. If you choose to use low-level call methods like call(), make sure to handle the possibility that the call will fail by checking the return value. This method returns false if the subcall encounters an exception, otherwise it returns true. There is no notion of legal call, if it compiles, it’s a valid contract. To handle call method we use the require() method to check for any unexpected error from the subcall.

address contractB = 0x4c9D562226ad4fb5EEA1596e92c02A146BE01693
function callChecked() public {
require(contractB.call()); //non-vulnerable
}

5. Reentrancy (Recursive Call Attack)

One of the major risks of calling external contracts is that they can take over the control flow. In the reentrancy attack (a.k.a. recursive call attack) calling external contracts can take over the control flow, and make changes to your data that the calling function wasn’t expecting. A reentrancy attack occurs when the attacker drains funds from the target contract by recursively calling the target’s withdraw function.

Let’s take a look at the victims code below:
mapping (address => uint) private userBalances;
function withdraw() public {
uint amountToWithdraw = userBalances[msg.sender];
require(msg.sender.call.value(amountToWithdraw)());
userBalances[msg.sender] = 0; 
}

When the contract fails to update its state (userBalances[msg.sender] = 0) prior to sending funds, the attacker can continuously call the withdraw function to drain funds from the contract. Anytime the attacker receives Ether, the attacker’s contract automatically calls its fallback function(contract’s function with no name that is automatically executed whenever the contract receives Ether and zero data), which is written to call the withdraw function again. At this point the attack has entered a recursive loop and the contract’s funds start to pour to the attacker balance. Because the target contract gets stuck calling the attacker’s fallback function, the contract is never able to update the attacker’s balance to 0. The target contract is tricked into thinking that nothing is going wrong wrong.

The recursive loop of a reentrancy attack

If we take a look at function withdraw() we can see that the victim’s contract uses address.call.value() to send funds to the msg.sender address. The victims contracts updates the state of userBalances[msg.sender] which makes victims contract vulnerable. Using the privileges that those vulnerabilities give an attacker could use an external contract to liquidate all of the victims contract funds.

Let’s take a look at the attacker’s code below :

address owner;
Victim v = Victim(0xae..); // Assigns Victims contract as “v”

//assign contract creator as owner
constructor (){
owner = msg.sender
}
//call withdraw function from the victims contract
function getWithdraw()public{
v.withdraw();
}
//fallback function that withdraws funds from the victim’s contract
function () public payable {
v.withdraw();
}
//send drained funds to attacker’s address 
function drainFunds() payable public{
owner.transfer(address(this).balance());
}

The fallback function of this contract function(), calls the withdraw function of victim’s contract ,to steal funds from the contract. On the other hand, function drainFunds() will be called at the end of the attack when the attacker wants to send all of the stolen funds to their own address. It should be clear now that reentrancy attacks take advantage of two particular smart contract vulnerabilities. The first being when victims contract state is updated after fund have been sent and not before. By failing to update the contract state prior to sending funds, the function can be interrupted mid-computation and the contract will be tricked into thinking the funds haven’t actually been sent yet. The second vulnerability is when the contract incorrectly uses address.call.value() to send funds.

To avoid reentrancy attacks on your contract :

  • Update contract balance before sending funds the the msg.sender address.
  • Use functions transfer() or send() when sending funds to an address instead of call.value() function because both transfer and send functions are limited to a stipend of 2.300 gas, enough to merely log an event and not multiple calls.
  • When using low-level calls, make sure all internal state changes are performed before the call is executed.
function withdraw() public {
uint amountToWithdraw = userBalances[msg.sender];
userBalances[msg.sender] = 0; 
require(msg.sender.transfer(amountToWithdraw));
}

6. Uncontrolled Resource Consumption (Resource Exhaustion)

Nowadays, more and more Dapps and instruments utilize blockchain technology and Ethereum smart contracts development. The more complex that the business logic behind the smart contract is, the more gas it requires to deploy it to the network. This is due to the fact that when the size of the contract code increases, the size of the contract’s bytecode also increases as well. Uploading a large amount of bytecode in the network requires a lot of gas. At this time, in Ethereum blockchainthere is a limitation of about 8 million gas limit per block. This means that the total amount of gas for all the transactions in a block also including contract deploy cannot exceed this number. As everyone in the Ethereum community knows, Gas is a necessary resource for the execution of smart contracts and transactions. If too little gas is specified, transaction may not get picked up by miners for further processing in a timely manner or die in the middle of processing. It is important to tune and optimize smart contract in order to minimize the amount of gas required.

When smart contracts are deployed or the functions inside them are called, the execution of these actions always requires a certain amount of gas, based of how much computation is needed to process and complete them. The Ethereum network specifies a block gas limit and the sum of all transactions included in a block can not exceed that limit. Programming patterns that are harmless in centralized applications can lead to DoS(Denial of Service) conditions in smart contracts when the cost of executing a function exceeds the block gas limit. For example modifying an array of unknown size, that increases in size over time, can lead to such a Denial of Service condition. Actions that require looping across the entire data structure should be consider to be avoided. If you absolutely have to loop over an array of unknown size, then you should plan for it to potentially take multiple blocks, and therefore require multiple transactions.

Reduce resource consumption by implementing the methods below:

  • Libraries — a library can be used to reduce the bytecode size of the contract, meaning that less gas will be used for the deployment of that contract. A library is uploaded to the Ethereum network just once, and it can be used by several contracts that require this functionality. We can also move a various functions from the contract to the library in order to reuse the code efficiently.
  • Using signatures to call functions — use call method to send message calls to other contracts. An advantage of this approach it that a smart contract does not have to store the bytecode of the called method. As a result, the compiled smart contract will not have any extra opcodes, that in in turn will reduce its size. Notice that is is not possible to get the return value of the function when the call command is used. And don’t forget to use require for handling error purposes.
  • Interfaces — the same result can be achieved by means of interface. In Solidity an interface is a special type of contract, limited to what the contract ABI can represent. With the use of interfaces in our smart contracts the code readability improves, and also the return values are handled.

7. Multiple External Calls

External calls can sometimes fail accidentally or deliberately, which can cause a Denial Of Service condition in the smart contract. To minimize the damage caused by such failures, it is always better to isolate each external call into its own transaction that can be initiated by the recipient of the call. This is especially relevant for payments, where it is better to let users withdraw funds from the contract rather than pushing funds to them automatically (this also reduces the chance of problems with the gas limit).It is recommended to follow call best practices:

  • Try avoiding combining multiple external calls in a single transaction, especially when calls are executed as part of a loop.
  • Always assume that external calls can fail.
  • Implement the contract logic to handle failed calls.
  • Avoid combining multiple calls in a single transaction, especially when calls are executed as part of a loop and always assume that external calls can fail. It is recommended to implement the contract logic that will handle failed calls.

“Avoid external calls when possible. Calls to untrusted contracts can introduce several unexpected risks or errors. External calls may execute malicious code in that contract or any other contract that it depends upon. As such, every external call should be treated as a potential security risk, and removed if possible.” — Ethereum Wiki

8. Diamond Problem

Solidity supports multiple inheritance, meaning that one contract can inherit several contracts. Multiple inheritance introduces ambiguity called Diamond Problem: if two or more base contracts define the same function, which one should be called in the child contract? Solidity deals with this ambiguity by using reverse C3 Linearization(an algorithm used primarily to obtain the order in which methods should be inherited in the presence of multiple inheritance), which sets a priority between base contracts. That way, base contracts have different priorities, so the order of inheritance matters. Neglecting inheritance order can lead to unexpected behavior. When inheriting multiple contracts, especially if they have identical functions, a developer should carefully specify inheritance in the correct order. The rule of thumb is to inherit contracts from more /general/ to more /specific/.

9. Transaction Ordering Assumptions

After submission transactions enter a pool of unconfirmed transactions and maybe included in blocks by miners in any order, depending on the miner’s transaction selection criteria, which is probably some algorithm focused for achieving maximum earnings from transaction fees. The order of transactions being included can be completely different to the order in which they are generated, therefore smart contract code cannot make any assumptions on transaction order.

Apart from unexpected results in contract execution, there is a possible attack vector in this, as transactions are visible in the mempool(Memory Pool) and their execution can be predicted. This maybe an issue in trading currencies , where delaying a transaction may be used for personal advantage by a rogue miner. In facet, simply being aware of certain transactions before they are executed can be used as advantage by anyone, not just miners.

Conclusion

Before starting to develop your smart contract, realize that there are already great developers in the community who have probably already developed and tested secure code something similar to what you’re trying to do in your own project so consider using some of their code. Don’t forget that the Ethereum Virtual Machine is still infancy and we are learning new things about EVM and Solidity every day, so always stay up to date on the best practices for Ethereum development and any new updates to the Ethereum ecosystem.

References


Follow us on Twitter, InvestFeed, Facebook, Instagram, LinkedIn, and join our Discord and Telegram.

Altcoin Magazine is a popular destination for cryptocurrency enthusiasts and blockchain researchers. We strive to become the largest, most accessible and easy to read a magazine full of news, articles, videos, and podcasts and will be the go-to place for monitoring of coins, crypto companies, projects, products, events, advisors and much, much more.

Read about our upcoming Altcoin Magazine Mastermind Event here.