Abstract
A breakdown of classic and prevalent smart contract vulnerabilities and exploitation techniques
Integer Overflow
The uint type in Solidity is widely used but inherently vulnerable to overflow since it's an unsigned integer. Prior to Solidity 0.8, there was no overflow protection, making contracts susceptible to manipulation.
Example: Token Balance Exploit
contract Token {
mapping(address => uint) balances;
uint public totalSupply;
function transfer(address _to, uint _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0); // No overflow check
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
}Exploit: Transferring values exceeding the balance causes an underflow, inflating the recipient's balance to an extreme value.
Solution: Use OpenZeppelin’s SafeMath for arithmetic operations with overflow checks.
Dynamic Array Length Underflow
Allowing users to manipulate array lengths can lead to underflows, enabling arbitrary storage writes.
Example:
contract AlienCodex {
bytes32[] public codex;
function retract() public { codex.length--; } // Underflow risk
}Impact: An underflow resets the array length to 2^256-1, allowing attackers to overwrite any storage slot.
Reentrancy Attacks
A classic exploit where recursive calls drain funds before state updates.
Vulnerable Code:
function withdraw(uint amount) public {
require(balances[msg.sender] >= amount);
(bool sent, ) = msg.sender.call{value: amount}(""); // External call
balances[msg.sender] -= amount; // State update after transfer
}Attack: A malicious contract’s fallback function re-calls withdraw(), bypassing checks due to delayed balance updates.
Prevention:
- Follow Checks-Effects-Interactions pattern.
- Use OpenZeppelin’s ReentrancyGuard.
👉 Learn how to secure your contracts against reentrancy
Pseudorandom Number Exploits
Block-Dependent Variables
Using block.timestamp or blockhash for randomness is predictable within the same block.
Example:
uint random = uint(keccak256(abi.encodePacked(block.timestamp))) % 2;Fix: Use verifiable randomness oracles like Chainlink VRF.
Blockhash Limitation
blockhash only works for the last 256 blocks; older blocks return zero.
Airdrop Exploitation
Attackers deploy multiple contracts to claim tokens repeatedly:
contract Token {
mapping(address => bool) airdropped;
function airdrop() public {
require(!airdropped[msg.sender]);
balances[msg.sender] += 100;
}
}Exploit: New contracts bypass the airdropped check, syphoning tokens.
Private Variable Reading
private variables are accessible via storage slot inspection:
contract Vault {
bytes32 private password; // Slot 0
}Tool: Use web3.eth.getStorageAt(contractAddress, slotIndex).
Delegatecall Hijacking
delegatecall executes external code in the caller’s context, risking storage collisions.
Example:
contract Preservation {
address public owner; // Slot 2
function setFirstTime(uint _time) public {
timeZone1Library.delegatecall(abi.encodeWithSignature("setTime(uint256)", _time));
}
}Attack: Modify timeZone1Library to point to a malicious contract, then overwrite owner.
👉 Secure delegatecall usage in your projects
FAQ
How can I prevent integer overflows?
Use OpenZeppelin’s SafeMath or Solidity ≥0.8 with built-in overflow checks.
Are private variables truly private?
No—they’re only hidden from public view but readable via storage slots.
What’s the safest way to generate randomness?
Off-chain oracles (e.g., Chainlink VRF) or commit-reveal schemes.
Why is delegatecall dangerous?
It allows external contracts to modify your contract’s storage unpredictably.
How do reentrancy attacks work?
Recursive calls exploit delayed state updates to drain funds.