10 smart contract vulnerabilities with code examples

Eman Herawy
Coinmonks
11 min readSep 26, 2023

--

A Comprehensive Guide for Understanding and Mitigating Security Risks in Blockchain Smart Contracts.

Disclaimer

This article, “10 smart contract vulnerabilities with code examples” was prepared as part of Ekolance’s smart contract audit training. It is intended for educational purposes only.

Introduction

This article is a compilation of 10 smart contract vulnerabilities, with code examples and explanations. It’s an attempt to create a comprehensive list of known vulnerabilities and to provide a resource for smart contract auditors and developers.

#1 Reentrancy

Description

Reentrancy is a vulnerability that allows an attacker to re-enter a function multiple times before the first function call is finished. so whenever the contract makes external call to other addresses, this is a possibility for reentrancy attack.This can lead to unexpected behavior, including reordering of transactions, and can be used to drain funds from a contract. Rareskills did a fantastic job explaining the different types of vulnerabilities in this article, as below:

  • When Ether is transferred, the receiving contract’s fallback or receive function is called. This hands control over to the receiver.
  • Some token protocols alert the receiving smart contract that they have received the token by calling a predetermined function. This hands the control flow over to that function.
  • When an attacking contract receives control, it doesn’t have to call the same function that handed over control. It could call a different function in the victim smart contract (cross-function reentrancy) or even a different contract (cross-contract reentrancy)
  • Read-only reentrancy happens when a view function is accessed while the contract is in an intermediate state.

Code Example:

function withdraw(uint amount) public{
if (credit[msg.sender]>= amount) {
require(msg.sender.call.value(amount)());
// here we are updating the credit after the external call, which is vulnerable to reentrancy attack
credit[msg.sender]-=amount;
}

A real-life example

Mitigation

  • Use the checks-effects-interactions pattern to avoid reentrancy attacks.
  • Use a reentrancy lock (ie. OpenZeppelin’s ReentrancyGuard).

Reference

The below 3 vulnerabilities are related to access control issues in smart contracts.

Access control vulnerabilities in smart contracts occur when there are flaws or weaknesses in the way a contract manages and enforces permissions or restrictions related to who can perform specific actions or operations within the contract. These vulnerabilities can lead to unauthorized access, manipulation, or control of the smart contract, potentially resulting in security breaches or financial losses. To simplify the explanation, we can say that access control is about controlling who calls a function.

#2 Missing Access Controls:

  • A smart contract may be missing access controls, allowing anyone to perform certain actions or operations that should be restricted to specific users or accounts. For example, a smart contract may allow anyone to withdraw funds from a contract or destruct the contract, when only the contract owner should be able to do so.

Code Example:

pragma solidity ^0.4.22;
contract SimpleSuicide {
function sudicideAnyone() {
selfdestruct(msg.sender);
}
}

A real-life example

  • 0xbad, a notorious MEV bot.An anonymous attacker noticed a flaw in the bots arbitrage contract code, and drained 0xbad’s wallet “1,101 ETH”. The attacker noticed that “callFunction,” which is the function called by the dYdX router as a part of flashloan execution, wasn’t properly protected and allowed arbitrary code to be executed. The attacker used this to get 0xbad to approve all of their WETH for spender on their contract. The attacker then simply transferred the WETH out to their address. The attack has a nice story to read TBH, you can check it here
  • Unprotected init() function was missing onlyOwner modifier, details.

Mitigation

  • Implement Proper Access Controls: Carefully design and implement access control mechanisms within your smart contract. Clearly define who can perform specific actions and enforce these rules in the contract’s code.

Reference

#3 Weak Access Controls:

Description

Contracts may implement access controls, but they are not robust enough to adequately restrict access to sensitive functions or data. Weak access controls often rely on simple checks that can be bypassed.

Code Example:

function claimAirdrop(bytes32 calldata proof[]) {
bool verified = MerkleProof.verifyCalldata(proof, merkleRoot, keccak256(abi.encode(msg.sender)));
require(verified, "not verified");
require(alreadyClaimed[msg.sender], "already claimed");
_transfer(msg.sender, AIRDROP_AMOUNT);
// "alreadyClaimed" is never set to true, so the claimant can issue call the function multiple times.
}

Mitigation

  • Implement Proper Access Controls: Carefully design and implement access control mechanisms within your smart contract. Clearly define who can perform specific actions and enforce these rules in the contract’s code.

A real-life example

Reference

#4 Privilege Escalation:

Description:

Privilege escalation vulnerabilities occur when a user can upgrade their own permissions within the contract, gaining access to functions or data they were initially restricted from. As common example of this is when an attacker takes ownership of the contract . Code Example:

A real-life example

  • Enzyme Finance bug: Enzyme Finance implements meta transaction via OpenGNS. In Enzyme Finance Implementation, there wasn’t any verification of the provided forwarder’s address in the paymaster making any malicious forwarder makes many relayCalls to the RelayHub until the target fund’s Vault is drained. The bug and fix are well explained in this article.
  • Implement Proper Access Controls: Carefully design and implement access control mechanisms within your smart contract. Clearly define who can perform specific actions and enforce these rules in the contract’s code.

Reference

Reference

#5 Missing or Improper Input Validation

Description

Missing or improper input validation is a vulnerability in a smart contract that occurs when the contract fails to adequately check and validate the data and parameters supplied by users or external sources before processing them. This vulnerability can have significant security implications as it may allow malicious actors to exploit the contract’s weaknesses. Here’s a breakdown of the key aspects of this vulnerability:

Code Example:

contract UnsafeBank {
mapping(address => uint256) public balances;
// allow depositing on other's behalf
function deposit(address _for) public payable {
balances[_for] += msg.value;
}
function withdraw(address from, uint256 amount) public {
require(balances[from] <= amount, "insufficient balance");
// attacker can withdraw more than their balance. Also attacker can withdraw from any address because no checks for authorization as well
balances[from] -= amount;
msg.sender.call{value: amout}("");
}
}

A real-life example

SushiSwap RouteProcessor2 : This contract contained a vulnerability where it didn’t properly validate the route parameter provided by the user to the processRoute function. This vulnerability allowed an attacker to set the route to point to a malicious, attacker-controlled pool. With a malicious pool, the attacker can call swapUniV3, which will set the variable lastCalledPool to the address of its pool and call the swap function of the malicious pool. That swap function will call uniswapV3SwapCallback, which validates the sender by checking to see that they are the lastCalledPool. Since this value is set to the malicious pool’s address, its callback is accepted. With the ability to call back into the uniswapV3SwapCallback function, the attacker can construct transactions that drain tokens from the account of users that set up approvals for the new RouteProcessor2 contract. The attackers managed to steal about $3.3 million, and the scope of the attack was limited by whitehack hacks that frontrun malicious transactions.

Mitigation

To mitigate this vulnerability, smart contracts should implement robust input validation checks. This includes verifying the sender’s authority, checking parameter values against predefined constraints, validating external data sources, and ensuring that input doesn’t exceed specified limits.

Reference

Description

A signature replay attack, also known as a replay attack, occurs when an attacker takes a valid signature from one transaction and reuses it to authenticate a different transaction in the same chain or in differnt b blockchain network.

Code Example:

// missing noonce 
// SPDX-License-Identifier: MIT
pragma solidity ^ 0.8.17;

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

contract Vault {
using ECDSA for bytes32;

address public owner;

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

function transfer(address to, uint amount, bytes[2]memory sigs) external {
bytes32 hashed = computeHash(to, amount);
require(validate(sigs, hashed), "invalid sig");

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

function computeHash(address to, uint amount) public pure returns(bytes32) {
return keccak256(abi.encodePacked(to, amount));
}

function validate(bytes[2]memory sigs, bytes32 hash) private view returns(bool) {
bytes32 ethSignedHash = hash.toEthSignedMessageHash();

address signer = ethSignedHash.recover(sigs[0]);
bool valid = signer == owner;

if (!valid) {
return false;
}

return true;
}
}


// missing chain id

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

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

contract Vault {
using ECDSA for bytes32;

address public owner;
mapping(bytes32 => bool) public executed;

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

function transfer(address to, uint amount, uint nonce, bytes[2] memory sigs) external {
bytes32 hashed = computeHash(to, amount, nonce);
require(!executed[hashed], "tx already executed");
require(validate(sigs, hashed), "invalid sig");

executed[hashed] = true;

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

function computeHash(address to, uint amount, uint nonce) public view returns (bytes32) {
return keccak256(abi.encodePacked(address(this), to, amount, nonce));
}

function validate(bytes[2] memory sigs, bytes32 hash) private view returns (bool) {
bytes32 ethSignedHash = hash.toEthSignedMessageHash();

address signer = ethSignedHash.recover(sigs[0]);
bool valid = signer == owner;

if (!valid) {
return false;
}

return true;
}
}

A real-life example

  • Yearn Vaults on ETH POW forks that use the same chainId and a DOMAIN_SEPARATOR value that is calculated at contract deployment are vulnerable to replay attacks, details.

Mitigation

  1. Use Nonces or Timestamps Incorporate nonces (random numbers) or timestamps into messages. Each message should have a unique nonce or timestamp, making it impossible for an attacker to replay the same message multiple times.
  2. Use a chain-specific signature scheme such as EIP-155, which includes the chain ID in the signed message. This will prevent transactions signed on one chain from being valid on another chain with a different ID.

Reference

#7 Sandwich attacks (front-running/back-running)

Description

Sandwich attacks, also known as front-running and back-running, are types of malicious trading strategies that exploit the order execution process on decentralized exchanges (DEXs) and automated market makers (AMMs) in the cryptocurrency space. These attacks involve manipulating the order book to profit from the price movements caused by a victim’s trade. Here’s how sandwich attacks work:

Front-Running: In a front-running attack, an attacker monitors pending transactions in a blockchain’s mempool to identify a large trade about to occur. The attacker quickly places their own buy or sell order with slightly better terms than the victim’s trade. This causes the victim’s trade to execute at a less favorable price, while the attacker’s trade benefits from the price change caused by the victim’s order.

Back-Running: Back-running is a similar concept but involves the attacker placing their trade just after the victim’s trade has been confirmed but not yet included in a block. This allows the attacker to profit from the price change caused by the victim’s trade before it becomes part of the blockchain.

A real-life example

Exotic culinary: Hypernative systems caught a unique Sandwich Attack against Curve Finance

Mitigation

  • Implement anti-front-running measures: Developers can implement technical measures to prevent front-running and other attacks. This might include measures such as batched orders, preventing attackers from seeing the details of pending trades.
  • Use private mempool “ dark pool” to hide your transactions

Reference

#8 Price oracle manipulation

Description:

Price oracle manipulation is a form of attack or manipulation in decentralized finance (DeFi) and blockchain-based systems that involves manipulating the data provided by a price oracle to gain an unfair advantage or exploit vulnerabilities in smart contracts. Price oracles are external data sources that supply real-world data, such as asset prices or exchange rates, to smart contracts for various purposes, including decentralized trading and lending. Price oracle manipulation can have serious consequences, as it can lead to financial losses and destabilize DeFi platforms.

Code Example:

It is a price manipulation issue, which can be exploited by donation to incorrectly calculate the price as shown in the following figures.

A real-life example

Mitigation

  • Properly implement Decentralized Oracles e.g Chainlink: DeFi platforms and smart contracts can use multiple independent oracles to aggregate price data. This reduces the risk of manipulation because an attacker would need to manipulate multiple oracles simultaneously, making it more challenging.

Reference

#9 Governance attacks

A governance attack, also known as a governance manipulation attack, is a type of attack that occurs within decentralized autonomous organizations (DAOs) and DeFi platforms where malicious actors attempt to manipulate or control the decision-making processes of the platform’s governance system for their own benefit. Governance in these contexts typically involves voting on proposals to make changes or decisions about the platform’s rules, parameters, and operations.
Governance attacks can take various forms, including allowing proposals to be executed without quorum, allowing execution of proposals without any voting step, or directly manipulating the votes of other participants. These attacks can compromise the decentralized nature of the protocol and lead to centralization of power, or result in financial benefits for the attackers. Governance attacks are particularly relevant in DAOs, where decision-making authority is distributed among token holders.

Code Example:

pragma solidity ^0.4.22;
pragma solidity ^0.8.13;

import "@oz/token/ERC20/ERC20.sol";
import "@oz/token/ERC20/utils/SafeERC20.sol";
import "@uniswap-v2-periphery/interfaces/IUniswapV2Router02.sol";
import "forge-std/console.sol";

contract Attacker {
using SafeERC20 for ERC20;

ERC20 usdc = ERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
ERC20 bean = ERC20(0xDC59ac4FeFa32293A95889Dc396682858d52e5Db);

function proposeBip() external payable {
swapEthForBean();
console.log(
"After ETH -> BEAN swap, Bean balance of attacker: %s",
bean.balanceOf(address(this)) / 1e6
);

depositAllBean();
console.log(
"After BEAN deposit to beanStalk, Bean balance of attacker: %s",
bean.balanceOf(address(this)) / 1e6
);

submitProposal();
}

function attack() external {
approveEverything();

flashloanAave();

console.log(
"Final profit, usdc balance of attacker: %s",
usdc.balanceOf(address(this)) / (10**usdc.decimals())
);

usdc.transfer(msg.sender, usdc.balanceOf(address(this)));
}
}

A real-life example

  • On April 17, 2022, Beanstalk suffered a $182 million governance attack where the attacker exploited the project’s protocol governance mechanism and drain funds from the pools. Source

Mitigation

  • Establish a robust, well-defined, and transparent governance framework that outlines the decision-making processes, voting mechanisms, and rules for participation. Implement secure and tamper-resistant voting systems that ensure the integrity of votes.

Reference

#10 Faulty Calculation

Description

Errors or inaccuracies in the mathematical or computational logic within smart contracts or DeFi protocols. These errors can lead to unintended consequences, financial losses, or vulnerabilities that can be exploited by malicious actors.

Code Example:

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

contract LendingPlatform {
mapping(address => uint256) public userBalances;
uint256 public totalLendingPool;

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

function withdraw(uint256 amount) public {
require(userBalances[msg.sender] >= amount, "Insufficient balance");

// Faulty calculation: Incorrectly reducing the user's balance without updating the total lending pool
userBalances[msg.sender] -= amount;

// This should update the total lending pool, but it's omitted here.

payable(msg.sender).transfer(amount);
}
}

A real-life example

  • On May 1, 2023, Level Finance was exploited due to a business logic issue and incorrect calculation, resulting in a total loss of $1.1M, link

Mitigation

  • security Audits: Conduct thorough security audits of smart contracts and DeFi protocols by reputable auditing firms. Auditors can identify coding errors and logic flaws.
  • Code Review: Review the smart contract code carefully to identify potential calculation errors. Enlist experienced developers or auditors for this task.
  • Test Environments: Use testnet environments to thoroughly test smart contracts and DeFi protocols before deploying them on the mainnet. Test for various scenarios, including edge cases.
  • Fuzzing testing : use fuzzing to generate all the possible values

These 10 vulnerabilities serve as lessons, highlighting the importance of robust security practices in blockchain development. It’s crucial to understand that these vulnerabilities can not only exist in isolation but are often combined in intricate ways to exploit smart contracts. By comprehending these weaknesses and the potential for their interplay, developers and auditors can better fortify blockchain systems, fostering a more secure and resilient landscape for the future of decentralized technology.

Reference

--

--

Eman Herawy
Coinmonks

Blockchain developer | @KERNEL fellow | @Chainlink developer expert | Devcon V Scholar Alumni @Ethereum