Web3 Security: 6 Smart Contract Vulnerabilities Developers Must Counter

Learn how to secure your Web3 project. It's easier than it seems.
Written by
Jiří Makovský
January 9, 2025
10
min. read
  • Web3 APIs are critical for dApps but present security risks. Based on the Chinalysis report, $1.8B was stolen in 2023 from cyberattacks. In 2024 it was $2.2B.
  • Common API risks: compromised keys, MitM attacks, and lack of input validation.
  • Use HTTPS, validate inputs, enforce role-based access, and implement rate limiting to fortify your Web3 API security.
  • Blockchain smart contracts are prone to vulnerabilities like reentrancy attacks and weak access controls, but these can be mitigated with secure coding and tools.

Decentralized applications (dApps) function by utilizing API calls. These facilitate interactions between different components of the Web3 ecosystem. However, if not properly secured, APIs can become vulnerable entry points for attackers. Because of the financial nature of cryptocurrencies, risks like unauthorized access, data breaches, and fund theft must be taken seriously by every developer or manager.

Smart contract vulnerabilities represent another weak point in Web3 infrastructure, often exposing exploitable security flaws that require careful mitigation.

In this article, we will focus on both aspects of Web3 security. What are some practices you should deploy? Be prepared for a lot of code examples!

Common Smart Contract Security Weaknesses

Smart contracts are an integral part of Web3 ecosystems, enabling decentralized, trustless operations. I believe there is no need to explain what they are, though we have a great article on the topic. Below, we outline some prevalent risks and offer actionable mitigation strategies.

Stop worrying at once. Get the most secure APIs on the market.

[.c-wr-center][.button-black]Start Now[.button-black][.c-wr-center]

Reentrancy Attacks

One of the most profound vulnerabilities in smart contract programming is reentrancy. It can occur when a malicious contract repeatedly calls back into the vulnerable contract before the initial execution is complete. The result is drained funds. 

A notable example is the DAO hack from 2016, which exploited a reentrancy vulnerability. It led to a loss of over $60 million in Ether. After a lengthy discussion, a decision was made to fork the Ethereum chain, effectively reverting the transactions. This is not, of course, anything a developer should rely on.

Example of Code Susceptible to a Reentrancy Attack:

pragma solidity ^0.8.0;

contract VulnerableContract {
    mapping(address => uint256) public balances;

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

    function withdraw(uint256 _amount) public {
        require(balances[msg.sender] >= _amount, "Insufficient balance");
        payable(msg.sender).transfer(_amount);
        balances[msg.sender] -= _amount; // Update balance after transfer
    }
}

How to Prevent Reentrancy

  • Use the Checks-Effects-Interactions pattern: Always update the contract state before interacting with external addresses.
  • Leverage OpenZeppelin’s ReentrancyGuard: Use the nonReentrant modifier to block reentrant calls.
  • Limit external calls: Minimize interactions with untrusted contracts or users.

Integer Overflows and Underflows

While Solidity 0.8+ largely eliminates these risks, earlier versions required libraries like SafeMath to prevent these errors. Before Solidity 0.8 was implemented, arithmetic operations that exceeded or went below the capacity of the variable type would wrap around, causing unpredictable behavior.

Example of an Overflow:

uint8 public count = 255;

function increment() public {
    count += 1; // Overflow, count becomes 0
}

How to Prevent Integer Issues

  • Upgrade to Solidity 0.8 or later: As has been said, the latest compiler versions have built-in overflow and underflow checks.
  • Use SafeMath library: This library by OpenZeppelin provides safe arithmetic functions, though it’s unnecessary mainly in Solidity 0.8+. You don’t need both.

[.c-box-wrapper][.c-box][.c-text-center]You might be interested in: MetaMask Integration: A Full Cross-Platform Guide[.c-text-center][.c-box][.c-box-wrapper]

Unchecked Call Results

Understanding gas refund mechanics is crucial when handling unchecked calls, as it can impact the financial viability of operations. If you fail to check the result of a call, it can lead to unexpected behavior or fund losses when interacting with external contracts.

Demonstration of Unchecked Call Result Vulnerability:

function sendEther(address payable recipient) public {
    recipient.call{value: 1 ether}(""); // No error handling
}

Mitigation Techniques

  • Check return values: Use require to safeguard the success of the call.
  • Avoid low-level calls: Utilize transfer or send for Ether transfers when possible.
  • Implement fallback mechanisms: Define default behaviors for failed transactions.

Insufficient Access Controls

Lack of robust access management can allow unauthorized users to execute sensitive functions.

Example of Poor Access Control:

function mint(address _to, uint256 _amount) public {
    // No restrictions on who can call this function
    _mint(_to, _amount);
}

How to Enhance Access Control

  • Use modifiers: Define onlyOwner or similar modifiers to restrict access.
  • Adopt role-based access control (RBAC): Leverage OpenZeppelin’s AccessControl library for managing roles.
  • Multi-signature wallets: For critical operations, require multiple signatures to execute.

Gas Limit Issues

Tools like Remix and Hardhat can help developers analyze gas costs and optimize their contracts effectively. Operations that exceed the block gas limit will fail, potentially locking funds or rendering the contract unusable.

How to Avoid Gas Limit Problems

  • Optimize gas usage:  Code efficiently and avoid unnecessary storage operations.
  • Break large loops: Split heavy operations into smaller, gas-efficient functions.
  • Fallback mechanisms: Implement functions to allow recovery of funds if an operation fails.

Security Best Practices for Smart Contracts

Some essential tips for developers to secure their smart contracts:

  • Audit Contracts: Regularly conduct professional code audits. If you are running a larger operation, such as a decentralized exchange, you could get a trusted company to do it for you.
  • Use Established Libraries: Depend on battle-tested libraries like OpenZeppelin to secure your application.
  • Enable Circuit Breakers: Implement mechanisms to pause or halt operations in case of suspicious activity.
  • Perform Penetration Testing: Simulate attacks to identify weak points.

Following these steps should significantly reduce the risk of your smart contract being successfully attacked.

Other Common Web3 Security Risks

Below, we’ll dive deeper into common API security risks while providing actionable tips, code snippets, and examples to help blockchain developers mitigate these threats effectively.

[.c-box-wrapper][.c-box][.c-text-center]You might be interested in: Smart Contracts: The Backbone of Decentralized Applications[.c-text-center][.c-box][.c-box-wrapper]

Compromised API Keys and How to Avoid Them

Losing control over your API keys can be a disaster since they are often the gateway to your application’s functionality. Your API keys can be compromised in several ways. A victim could hardcode API keys into frontend repositories, or they might share the key publicly, for instance, on GitHub. Another risk is the classic human error, which could lead to keys being exposed when sharing API keys with other team members.

To give you an example, in 2023, an API key initially leaked on GitHub was used by attackers to access sensitive blockchain operations. The result? The loss of $100,000 in assets.

We have several ways for you to minimize risks. First, this might be a no-brainer, but store keys safely. You can utilize environment variables or secrets management tools like AWS Secrets Manager or Vault. Implementing a key rotation policy is also important. Lastly, consider using role-based permissions. These limit what each API key can do, resulting in the attacker never scoring full access.

[.c-box-wrapper][.c-box][.c-text-center]Read more about key safety in the Tatum documentation.[.c-text-center][.c-box][.c-box-wrapper]

Securing an API Key with Environment Variables in Node.js

In this example, three conditions must be met. First, replace TATUM_API_KEY with your key and set up the .env file. Then, replace the endpoint https://api.tatum.io/v3/blockchain, which is a placeholder. All supported blockchain endpoints are listed here. Lastly, make sure the dependencies axios and dotenv are installed and running.

require('dotenv').config();  // Load environment variables
const axios = require('axios');
const apiKey = process.env.TATUM_API_KEY;  // Fetch the API key from environment variables
// Example Tatum API endpoint (replace with the actual endpoint you want to use)
const endpoint = 'https://api.tatum.io/v3/blockchain';  // Example endpoint for blockchain data
async function fetchBlockchainData() {
  try {
    // Send the POST request with the correct header and request body
    const response = await axios.post(endpoint, {
      jsonrpc: '2.0',
      method: 'eth_blockNumber', // Example method for Ethereum block number
      params: [],
      id: 1
    }, {
      headers: { 'x-api-key': apiKey },  // API key is now passed securely
    });
    console.log(response.data);  // Log the response data to the console
  } catch (error) {
    console.error('Error fetching data:', error.message);  // Handle errors
  }
}
// Call the function to fetch blockchain data
fetchBlockchainData();

Relevant Tips for Web3 API Security

  • Your .env file should be excluded from version control by adding it to .gitignore.
  • Use CI/CD tools to pass environment variables during deployment securely.

[.c-box-wrapper][.c-box][.c-text-center]You might be interested in: Top Blockchain Use Cases (2024)[.c-text-center][.c-box][.c-box-wrapper]

Man-in-the-Middle (MitM) Attacks

MitM attacks occur when an attacker intercepts and manipulates data transmitted between your dApp and the API.

They result from API calls over unencrypted channels (e.g., HTTP instead of HTTPS). What can add to the problem is the lack of certificate validation in the client-server communication.

Avoiding this happening to you consists of enforcing HTTPS for all API communication, validating SSL certificates on both ends and using mutual TLS (mTLS) for added security. mTLS works by requiring both the client and server to authenticate each other.

Enforcing HTTPS in Express.js

const https = require('https');
const fs = require('fs');
const app = require('express')();

// Load SSL certificates
const options = {
  key: fs.readFileSync('private-key.pem'),
  cert: fs.readFileSync('certificate.pem'),
};

// Create HTTPS server
https.createServer(options, app).listen(443, () => {
  console.log('Server is running securely on port 443');
});

Lack of Input Validation

APIs often work with user-provided data. That makes them vulnerable to attacks such as SQL injection or XSS. Those can occur when input fields in API calls are not sanitized or validated. Malicious payloads get executed within the application. One DeFi suffered a reentrancy attack when unvalidated input allowed recursive calls to drain funds from the smart contract.

To guard your API against breaches stemming from missing input validation, validate inputs with a predefined schema. Sanitize all data to remove harmful content and enforce these rules consistently with dedicated libraries.

Using Joi for Input Validation in Node.js

const Joi = require('joi');

// Define validation schema
const schema = Joi.object({
  username: Joi.string().alphanum().min(3).max(30).required(),
  email: Joi.string().email().required(),
  amount: Joi.number().positive().required(),
});

// Validate input
function validateInput(data) {
  const { error } = schema.validate(data);
  if (error) throw new Error(`Validation error: ${error.details[0].message}`);
}

// Example usage
try {
  validateInput({ username: 'user123', email: 'test@example.com', amount: 100 });
  console.log('Validation passed!');
} catch (error) {
  console.error(error.message);
}

[.c-box-wrapper][.c-box][.c-text-center]You might be interested in: Web3 Dapp Hosting: Components, Preferences, and Best Practices[.c-text-center][.c-box][.c-box-wrapper]

Inadequate Authentication and Authorization

Authentication ensures that users are who they claim to be. Authorization, on the other hand, controls what actions they can perform. Weak mechanisms in either can lead to serious vulnerabilities.

How exactly can a lack of authentication and authorization be exploited? For instance, missing multi-factor authentication when connecting to the application can lead to vulnerabilities. Using shared API keys across multiple users is another, unfortunately, a common mistake. Poor session management can be exploited as well.

Were the attacker to exploit any of this, they could abuse inadequate role-based access controls, which allow hackers to escalate privileges and execute unauthorized transactions. To minimize risk to your dApps, use token-based authentication such as JWT, enforce role-based permissions to limit access, and enable multi-factor authentication for sensitive operations.

Role-Based Access Control

function checkRole(role) {
  return (req, res, next) => {
    if (req.user.role !== role) {
      return res.status(403).json({ message: 'Access denied' });
    }
    next();
  };
}

// Protect an admin-only route
app.get('/admin', checkRole('admin'), (req, res) => {
  res.send('Welcome, Admin!');
});

Rate Limiting and Throttling

Protecting your API from overload requires rate limiting and throttling. When APIs allow unlimited requests, they’re vulnerable to abuse. Introduce request caps per IP and enhance security with captchas or anti-bot mechanisms for public APIs.

Applying Rate Limiting in Express.js

const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  windowMs: 10 * 60 * 1000, // 10 minutes
  max: 100, // Limit each IP to 100 requests per window
  message: 'Too many requests from this IP, please try again later.',
});

app.use('/api', limiter);

Deficient Logging and Monitoring

Failure to maintain detailed API logs gives attackers the upper hand. Blockchain’s critical timing requirements mean headers, payloads, and timestamps must always be stored. Ignoring access or error logs is an open invitation to threats. Implement full-spectrum logging and employ monitoring solutions like Tatum’s API monitoring or third-party systems like Splunk and ELK Stack.

Simple Logging Middleware

app.use((req, res, next) => {
  console.log(`${req.method} request to ${req.url} at ${new Date().toISOString()}`);
  next();
});

Best Practices for Securing Web3 APIs

Achieving Secure API Key Management

  • Environment Variables: Store API keys in environment variables to prevent exposure in code repositories.
  • Regular Rotation: Rotate API keys periodically and revoke those no longer in use. More info in the documentation.
  • Access Control: To limit potential damage from a compromised key, assign minimal permissions to each API key. This must be done on your side.

Example in Node.js:

// Load environment variables
require('dotenv').config();

// Access API key
const apiKey = process.env.API_KEY;

Implement HTTPS

  • TLS Certificates: Obtain and configure TLS certificates to enable HTTPS. This will safeguard your communication.
  • Redirect HTTP to HTTPS: Configure your server to redirect all HTTP requests to HTTPS to enforce secure connections.

Input Validation and Sanitization

  • Server-Side Validation: Validate all inputs on the server side to prevent malicious data from compromising the system.
  • Use Validation Libraries: Utilize libraries like Joi or express-validator to enforce input schemas.

Example in Express.js using express-validator:

const { body, validationResult } = require('express-validator');

app.post('/api/data', [
  body('username').isAlphanumeric().withMessage('Username must be alphanumeric'),
  body('email').isEmail().withMessage('Invalid email address')
], (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }
  // Proceed with processing the valid input
});

[.c-box-wrapper][.c-box][.c-text-center]You might be interested in: How to Build a Web3 Blockchain Cryptocurrency Wallet[.c-text-center][.c-box][.c-box-wrapper]

Robust Authentication and Authorization

  • OAuth 2.0: This is the ultimate safeguard for user logins and controlled authorization in modern applications.
  • JSON Web Tokens (JWT): Adopt JWTs for a secure, stateless authentication method that scales easily and fits seamlessly into modern application architectures.

Example of generating a JWT in Node.js:

const jwt = require('jsonwebtoken');

const payload = { userId: user.id };
const secret = process.env.JWT_SECRET;
const token = jwt.sign(payload, secret, { expiresIn: '1h' });

Rate Limiting and Throttling

  • Prevent Abuse: Implement rate limiting to prevent abuse and potential denial-of-service attacks.
  • Tools: Use middleware like express-rate-limit in Express.js applications.

Example using express-rate-limit:

const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100 // limit each IP to 100 requests per windowMs
});

app.use('/api/', limiter);

Regular Security Audits

  • Code Reviews: Conduct regular code reviews to identify and fix vulnerabilities. If you are managing a business, audit transparency can be worth taking into account.
  • Penetration Testing: Perform penetration testing to assess the security posture of your APIs.

Leveraging Tatum for Enhanced API Security

Develop next-generation Web3 applications easily, powered by APIs that put security front and center.

  • Reduce the risk of key exposure to safeguard a higher level of security for sensitive blockchain operations.
  • Interact with multiple blockchains through a single, secure API. Minimize the complexity and potential vulnerabilities associated with multi-chain interactions.
  • Utilize purchasable monitoring tools to detect and respond to suspicious activities promptly.

[.c-wr-center][.button-black]Get Started[.button-black][.c-wr-center]

Notable Web3 Security Breaches

Here is a quick table. Below you will find all the details.

To get a more accurate picture, here are some significant breaches:

Poly Network Hack (August 2021)

The August 2021 Poly Network hack revealed the dark side of cross-chain protocols. Attackers manipulated a vulnerability to override contract calls, escaping with $610 million. Surprisingly, the hacker returned the loot, citing ethical motivations, yet the event served as a grim warning about the fragile state of cross-chain interoperability.

Ronin Network Breach (March 2022)

With compromised private keys, attackers forged fake withdrawals, stealing approximately $620 million from the Ronin Network, which supports the Axie Infinity game. The breach went unnoticed for six days, emphasizing the importance of vigilant monitoring and robust key management practices. At the time, it was one of the largest incidents, even catching the attention of mainstream media.

One of many articles about this particular hack. This one is from Cointelegraph.

Nomad Bridge Exploit (August 2022)

A smart contract misconfiguration triggered a $190 million exploit on the Nomad token bridge. The simplicity of copying transaction call data invited a wave of attackers, creating a chaotic situation.

Mango Markets Attack (October 2022)

An attacker manipulated the price oracle of Mango Markets, a DeFi platform, inflating collateral value and borrowing $114 million, effectively draining the platform's funds. The hacker later negotiated a settlement and returned some funds. This whole ordeal highlights significant vulnerabilities in price oracle mechanisms. We have a great article about oracles if you want to learn more!

Euler Finance Exploit (March 2023)

A flash loan exploit on Euler Finance led to a loss of a whopping $197 million. The attacker found a vulnerability in the platform's donation mechanism. The platform’s operator engaged with the attacker, who returned a significant portion of the stolen funds.

Emerging Web3 Security Threats in 2024

We figured it would be worthwhile to address which vulnerabilities could quickly become relevant in the future.

  • Cross-Chain Vulnerabilities:
    With more dApps integrating cross-chain, attackers can exploit weak links. Usually, those would be bridges. For example, in 2022, a cross-chain bridge exploit resulted in losses exceeding $600 million.
  • Quantum Computing:
    Quantum computers are not an immediate threat. But as everyone on the blockchain industry will tell you, they pose a significant risk. Quantum advancements may render existing cryptographic standards obsolete, requiring developers to adopt quantum-resistant algorithms.
  • AI-Powered Attacks:
    Hackers use AI to automate phishing campaigns and exploit vulnerabilities faster than ever.

Web3 API Security Checklist

If you wish to find out if your application is secure quickly, apply this checklist.

  • API Key Management: Store keys in environment variables and rotate them regularly.
  • Encrypt Communication: Use HTTPS and enforce TLS for all API traffic. This is a basic hack mitigation strategy, so do not underestimate it.
  • Input Validation: All incoming data is sanitized and validated to prevent injection attacks.
  • Authentication: Leverage OAuth 2.0 or JWTs for secure user authentication.
  • Rate Limiting: Cap API requests per user/IP. Abuse will be partly mitigated.
  • Logging and Monitoring: Log all API access attempts and monitor for suspicious behavior. If you are feeling like it, this process can be automated as well. 
  • Regular Audits: Schedule code reviews, penetration tests, and security audits and do them regularly. Transparency or outsourcing to a trusted company will award you bonus points.

Blockchain development is as easy as an API call with the right tools. Sign Up for Free.