Web3 Security: 6 Smart Contract Vulnerabilities Developers Must Counter

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!
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.
[.c-wr-center][.button-black]Start Now[.button-black][.c-wr-center]
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
}
}
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
}
[.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]
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
}
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);
}
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.
Some essential tips for developers to secure their smart contracts:
Following these steps should significantly reduce the risk of your smart contract being successfully attacked.
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]
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]
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();
[.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]
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.
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');
});
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.
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]
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.
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!');
});
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.
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);
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.
app.use((req, res, next) => {
console.log(`${req.method} request to ${req.url} at ${new Date().toISOString()}`);
next();
});
Example in Node.js:
// Load environment variables
require('dotenv').config();
// Access API key
const apiKey = process.env.API_KEY;
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]
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' });
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);
Develop next-generation Web3 applications easily, powered by APIs that put security front and center.
[.c-wr-center][.button-black]Get Started[.button-black][.c-wr-center]
Here is a quick table. Below you will find all the details.
To get a more accurate picture, here are some significant breaches:
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.
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.
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.
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!
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.
We figured it would be worthwhile to address which vulnerabilities could quickly become relevant in the future.
If you wish to find out if your application is secure quickly, apply this checklist.
Blockchain development is as easy as an API call with the right tools. Sign Up for Free.
Build blockchain apps faster with a unified framework for 60+ blockchain protocols.