Lejdi Prifti

0 %
Lejdi Prifti
Software Engineer
Web3 Developer
ML Practitioner
  • Residence:
    Albania
  • City:
    Tirana
  • Email:
    info@lejdiprifti.com
English
Italian
French
Spring
React & Angular
Machine Learning
Docker & Kubernetes
AWS & Cloud
Team Player
Communication
Time Management
  • Java, JavaScript, Python
  • AWS, Kubernetes, Azure
  • Bootstrap, Materialize
  • Stylus, Sass, Less
  • Blockchain, Ethereum, Solidity
  • React, React Native, Flutter
  • GIT knowledge
  • Machine Learning, Deep Learning
0

No products in the basket.

Security Vulnerabilities in Ethereum Smart Contracts

25. December 2023

Introduced in 2015 by Vitalik Buterin, Ethereum is a decentralized, open-source blockchain that enables the creation of smart contracts – self-executing contracts with the terms of the agreement directly written into code.  The decentralized blockchain’s market capitalization of over 270 billion US dollars, as reported by CoinMarketCap, attests to the significance of this technology. My goal in writing this article is to give a precise overview of security vulnerabilities that are currently known to exist in the Ethereum smart contract ecosystem.

Table of Contents

Introduction to Ethereum

Ethereum’s basic element is the account. Each account has four fields, which are nounce, balance, storage and code. The nounce is a transaction counter, increased by one for every new transaction sent by the account. The balance is the amount of Ether the account owns. The storage represents the memory space for code and its execution. Finally, the code is where the smart contract code is stored.

There are two types of accounts, external and contract accounts. An external account is controlled by public-private key pairs owned by human account holders, while a contract account is controlled by their code. The major difference between the two is where the code field is empty or not. If it is not empty, then we are dealing with a contract account. 

External accounts are able to initiate actions that alter the state of the EVM. These actions are called transactions. Execution of transactions is not free. The reason behind this is because the world state change (transaction) that must be universally accepted requires computing resources that consume a lot of energy.

Gas was created for transaction execution and smart-contract interactions. It represents the units that the initiator needs to pay for the transaction executions. Gas limit is the maximum amount of gas that the initiator is willing to pay, while the gas price is the amount of Ether that the intiator is willing to pay for each unit of gas. 

Smart Contract Lifecycle

A smart contract goes through four phases, that are creation, deployment, execution and completion.

The Creation phase represents the moment of writing of the smart contract in a high-level programming language such as Solidity. Afterwards, the smart contract gets compiled to opcodes, which are low-level instructions used by the EVM. Finally, the opcodes are encoded to bytecodes for storage reasons. 

During the Deployment phase, the developer initiates a transaction that contains the bytecodes stored in a field called init in the transaction structure. This action, which might be performed using tools such as Truffle, returns another fragment of the code that will be stored in the EVM running environment and will be executed later.

In the Execution stage, a smart contract is a running program like a process or thread in a stand-alone computer. It will receive transactions and the data that will be passed to the contract as parameters. The EVM executes the instruction one-by-one until finished or the gas limitation is reached. This process happens at the time a new block is mined.

In the Completion phase, states are updated and recorded in blockchains alongside transactions following the completion of a transaction.

Ethereum Smart Contract Vulnerabilities

In this section, I will give a general description of the vulnerabilities, and for demonstration, I will include Solidity code for a few of them.

1. Re-Entrancy

It describes a situation where contract A calls contract B. However, contract B could call A back and execute A’s call again due to the fallback mechanism of Solidity.

The fallback function will be executed when calls from other contracts cannot find a matching function. When the caller uses the call function without giving any function signature, callee’s fallback function will be activated. Therefore, this function could call the caller’s function to re-enter the caller.

Imagine we have the folllowing scenario. VulnerableContract has a simple deposit and withdraw mechanism. The AttackerContract is designed to exploit reentrancy by repeatedly calling the withdraw function of the VulnerableContract in its fallback function. 

				
					// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract VulnerableContract {
    mapping(address => uint) private balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint _amount) public {
        require(balances[msg.sender] >= _amount, "Insufficient balance");
        
        // imagine an external call here, like calling another contract
        (bool success, ) = msg.sender.call{value: _amount}("");
        require(success, "Transfer failed");

        balances[msg.sender] -= _amount;
    }

    function getBalance() public view returns (uint) {
        return balances[msg.sender];
    }
}

// an attacker contract that exploits reentrancy
contract AttackerContract {
    VulnerableContract vulnerableContract;

    constructor(address _vulnerableContractAddress) {
        vulnerableContract = VulnerableContract(_vulnerableContractAddress);
    }

    // this function is designed to exploit reentrancy
    function attack() public payable {
        // call the withdraw function of the vulnerable contract
        vulnerableContract.withdraw(1 ether);
    }

    // fallback function to keep the attack going
    receive() external payable {
        if (address(vulnerableContract).balance >= 1 ether) {
            vulnerableContract.withdraw(1 ether);
        }
    }

}

				
			

Among the countermeasures proposed, have been the restriction of usage of the call function whenever possible. Instead of using the call function to send Ether to other accounts, the transfer function is more secure. It sends only 2300 gas with external calls, therefore the called contract will not have enough gas to reenter.

Additionally, adding a “lock” could be beneficial. The “lock” is a state variable that shows the state of external calls and permits the external calls only if the state is right. 

Finally, it is advised to make all the state changes before the call function is executed.

2. Arithmetic Issues

In Solidity, integer type has a specific range. The int and uint types represent signed and unsigned integers, respectively. The range of these integer types is determined by the number of bits used to represent them.

For uint (unsigned integers), the range is from 0 to 2^n – 1, where n is the number of bits. For example, uint256 has a range from 0 to 2^256 – 1.

For int (signed integers), half of the range is used for positive values, and the other half is used for negative values, including 0. For int256, the range is from -2^255 to 2^255 – 1.

When performing addition, subtraction, or storing user input with integer variables that contain value limitations, overflow/underflow can occur. If the value of variables goes beyond the upper or lower bound, the value will wrap to the other side of the bound. 

The code below presents two examples: an unsafe addition (unsafeAdd) and a safe addition (safeAdd). The crucial distinction lies in the safeAdd function, where a value check is performed using the require() operation. This check ensures that the sum of a and b is greater than or equal to the original value of a. If an arithmetic issue, such as overflow or underflow, were to occur, the sum (a + b) could potentially wrap around to the other side of the bound, as mentioned earlier. The require() statement acts as a safeguard, preventing such issues by verifying that the result does not violate this expected constraint.

				
					// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SafeMath {
    function unsafeAdd(uint256 a, uint256 b) public pure returns (uint256) {
        return a + b;
    }

    function safeAdd(uint256 a, uint256 b) public pure returns (uint256) {
        require(a + b >= a, "SafeMath: Addition overflow");
        return a + b;
    }
}
				
			

To counter this popular vulnerability, it is recommended to use audited libraries such as SafeMath by OpenZeppelin.

3. Delegatecall to Insecure Contracts

In Solidity, delegatecall is a low-level function that allows a contract to execute code from another contract while maintaining the context of the calling contract. This means that the called contract can access the storage, balance, and address of the calling contract. 

In the example below, Caller gives Callee permission to alter its own state variables. In this case, Callee is able to change the value variable.

The delegatecall function must be used with caution due to this preservation of context it offers. 

				
					// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Caller {
    uint public value;

    function setValue(address _target, uint _newValue) external {
        // using delegatecall to execute the setValue function of the target contract
        (bool success, ) = _target.delegatecall(abi.encodeWithSignature("setValue(uint256)", _newValue));
        require(success, "Delegate call failed");
    }
}

contract Callee {
    uint public value;

    function setValue(uint _newValue) external {
        value = _newValue;
    }
}

				
			

In Solidity, to address security vulnerabilities related to delegate calls, developers often use various patterns and best practices, and one common approach is to follow the “storage separation” pattern. This pattern involves separating the storage variables into a separate contract (often referred to as a library or a data contract) that is only responsible for managing data and doesn’t contain any logic.

By doing this, when using delegate calls, the called contract can modify the storage of its own data contract without affecting the storage of the calling contract. This helps prevent unintended state modifications and enhances the security of the system.

4. Selfdestruct

The selfdestruct operation in Solidity provides a way to remove contracts from the following blocks. However, this operation presents a vulnerability if senders are not updated with the new contract’s address. Some will continue to send Ether to the destructed contract. Therefore, this will cause the loss of Ether. 

When managing external calls, a careful approach is necessary to mitigate this risk. To avoid inadvertent Ether transfers to the out-of-date contract, developers should make sure that all pertinent parties are promptly informed of the revised contract address. Establishing transparent channels of communication and documentation is also essential to reduce the likelihood of Ether loss and preserve the decentralized application’s security.

5. Freezing Ether

The purpose of a smart contract is to transfer and receive Ether. One that can only accept Ether but has no way to send Ether out is said to be in a freezing-Ether state. If the contract does not specify any withdraw functions, it may be avaricious and freeze the Ether transferred to its address. 

The example shown below provides the required method for depositing Ether; however, it cannot be taken back. The Ether funds will be locked indefinitely.

Developers must pay attention to the contract during design phase. 

				
					// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract EtherReceiver {
    // event emitted when Ether is received
    event EtherReceived(address sender, uint256 amount);

    // fallback function to receive Ether
    receive() external payable {
        emit EtherReceived(msg.sender, msg.value);
        // Ether is accepted but not withdrawn
    }

    // function to get the contract's balance
    function getContractBalance() external view returns (uint256) {
        return address(this).balance;
    }
}

				
			

6. Randomness Generation

Ethereum and Solidity contracts have no true source of entropy. If historical blocks’ timestamp or hash is used for randomness generation, attackers can use the same random number generation process to obtain the same result because historical blocks never change. If using the future blocks, the process may be susceptible to malicious miners who can intentionally choose transactions and their execution order. 

Take a lottery contract, for instance, wherein participants buy tickets and only a lucky winner receives the payout. To select a winner, randomization is required. A miner could notice that the contract employs an entropy source and attempt to manipulate it to their advantage in order to improve their odds.

Block-related data should therefore not be used as an entropy source for randomness. The source ought to exist outside of the blockchain framework.

7. tx.origin

tx.origin stores the original caller’s address of a transaction and is always an external account.

If Bob is calling contract A, and contract A is calling contract B, tx.origin is Bob. However, msg.sender for contract B is contract A. msg.sender is the immediate caller. 

It is recommended to never use tx.origin in identity verification or authentication.

				
					// Contract A
pragma solidity ^0.8.0;

contract ContractA {
    address public originalCaller;

    // Function in Contract A that calls Contract B
    function callContractB(address contractBAddress) external {
        // Save the original caller (Bob)
        originalCaller = tx.origin;

        // Call Contract B
        ContractB(contractBAddress).doSomething();
    }
}

// Contract B
contract ContractB {
    address public actualCaller;

    // Function in Contract B
    function doSomething() external {
        // Save the actual caller (Contract A)
        actualCaller = msg.sender;
    }
}

				
			

8. Mishandled Exceptions

When using low-level functions, a false return value should be noted and properly handled. Low-level functions such as call, delegatecall, send , etc. return a boolean that represents if the call has been successful or not. It will return false for failure and true for success. The security of a smart contract is affected if the return value of a low-level function is not handled properly. 

The best approach for mitigating such Ethereum smart contract vulnerabilities is to avoid using low-level functions. In case developers must use them, they should be aware of checking the return values and handle the false ones.

				
					// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Example {
    address public owner;

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

    function transferOwnership(address newOwner) external onlyOwner {
        (bool success, ) = newOwner.call(abi.encodeWithSignature("acceptOwnership()"));

        // checking the return value and handling it
        require(success, "Ownership transfer failed");

        // update the owner
        owner = newOwner;
    }
}

				
			

9. Timestamp Dependence

Time restrictions are used by a wide range of applications to determine whether actions are required or allowed in a certain state. Time constraints are usually executed by using block timestamps. 

A block’s timestamp can be ascertained to a certain extent by the miner who initiates a new block on the blockchain—the instantiation of a contract. Some applications that work properly under time constraints are vulnerable to malevolent miners since the miner has the ability to determine timestamps on transactions.

Remember, block.number is a better choice than block.timestamp.Block timestamps have the potential to lead to smart contract vulnerabilities in Ethereum.

10. Transaction Order Dependence

The sequence in which transactions are executed determines the world or contractual state. If the order is changed in a real-world scenario where a transaction is contingent on the contract’s status, it could lead to serious issues like purchasing or selling goods at inflated prices.

11. Default Visibility

If a developer does not specify an internally used function as private, that function could be called from outside the contract. This could possibly lead to unexpected operation escalation.

The most important countermeasure for this type of Ethereum smart contract vulnerabilities is to pay attention during development. Always determine specifiers. 

12. Owner Operations

These problems arise when contract owners have certain rights and are required to fulfill certain obligations in order for their contract to move forward to the next state.

The whole contract will not work if a privileged user—such as the owner—loses their private key or stops being active. The contract establishes a single point of failure that is prone to human error because it depends on the owner’s sole address.

13. External Contract Referencing

Similar to microservices, smart contracts also require communication between one another. As a result, a security flaw is produced if the incorrect address is given while referencing another smart contract, like a library. It could be a malicious contract that calls from an incorrect address.

Sometimes, it is better to hard code the external addresses into contract’s code. Moreover, the new keyword could be used to create contracts instead of deployment inputs.

14. Short Parameter Issue

Prior to being passed to the smart contract, the parameters are encoded. The encoded parameter is 32 bytes, and if necessary, the EVM will pad 0 at the end and concatenate all the encoded arguments together.

Following the EVM padding, if the first parameter is not long enough—for example, 30 bytes—a 2-byte left shift will result. The second parameter’s value will rise as a result of the shift and padding.

That’s why parameters must be validated first and their order matters.

15. Costly Loop

Looping over a resource-intensive function can cause the contract to lose all of its gas. Contracts can only use however much gas the user sends with a transaction.

If a costly loop drains the contract of gas then the contract will fail. The smart contract below demonstrates how the gasGrevingOperation  might drain the contract from gas by simulates the expensive operation within the loop.

				
					// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract CostlyLoopExample {
    address public owner;
    uint256 public numIterations;

    event LoopCompleted();

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

    constructor(uint256 _numIterations) {
        owner = msg.sender;
        numIterations = _numIterations;
    }

    function executeCostlyLoop() external onlyOwner {
        for (uint256 i = 0; i < numIterations; i++) {
            gasGrievingOperation();
        }
        emit LoopCompleted();
    }

    function gasGrievingOperation() internal {
        uint256 c = 1 + 2;

        for (uint256 i = 0; i < 100; i++) {
            c = c + i;
        }
    }
}

				
			

All the situations mentioned above lead to Ethereum smart contract vulnerabilities and attrack financially motivated attackers to exploit such vulnerabilities. It is important that smart contract are developed correctly and with attention. 

If you need your smart contract audited, you might contact me at any time. Furthermore, I am developing a smart contract vulnerability detection tool with AI and it will soon be online. Read more here.

Thank you for reading!

Buy Me A Coffee
Posted in BlockchainTags:
Write a comment