Solidity Security
Master smart contract security best practices, vulnerability prevention, and secure Solidity development patterns.
When to Use This Skill
-
Writing secure smart contracts
-
Auditing existing contracts for vulnerabilities
-
Implementing secure DeFi protocols
-
Preventing reentrancy, overflow, and access control issues
-
Optimizing gas usage while maintaining security
-
Preparing contracts for professional audits
-
Understanding common attack vectors
Critical Vulnerabilities
- Reentrancy
Attacker calls back into your contract before state is updated.
Vulnerable Code:
// VULNERABLE TO REENTRANCY contract VulnerableBank { mapping(address => uint256) public balances;
function withdraw() public {
uint256 amount = balances[msg.sender];
// DANGER: External call before state update
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] = 0; // Too late!
}
}
Secure Pattern (Checks-Effects-Interactions):
contract SecureBank { mapping(address => uint256) public balances;
function withdraw() public {
uint256 amount = balances[msg.sender];
require(amount > 0, "Insufficient balance");
// EFFECTS: Update state BEFORE external call
balances[msg.sender] = 0;
// INTERACTIONS: External call last
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
Alternative: ReentrancyGuard
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SecureBank is ReentrancyGuard { mapping(address => uint256) public balances;
function withdraw() public nonReentrant {
uint256 amount = balances[msg.sender];
require(amount > 0, "Insufficient balance");
balances[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
- Integer Overflow/Underflow
Vulnerable Code (Solidity < 0.8.0):
// VULNERABLE contract VulnerableToken { mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) public {
// No overflow check - can wrap around
balances[msg.sender] -= amount; // Can underflow!
balances[to] += amount; // Can overflow!
}
}
Secure Pattern (Solidity >= 0.8.0):
// Solidity 0.8+ has built-in overflow/underflow checks contract SecureToken { mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) public {
// Automatically reverts on overflow/underflow
balances[msg.sender] -= amount;
balances[to] += amount;
}
}
For Solidity < 0.8.0, use SafeMath:
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
contract SecureToken { using SafeMath for uint256; mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) public {
balances[msg.sender] = balances[msg.sender].sub(amount);
balances[to] = balances[to].add(amount);
}
}
- Access Control
Vulnerable Code:
// VULNERABLE: Anyone can call critical functions contract VulnerableContract { address public owner;
function withdraw(uint256 amount) public {
// No access control!
payable(msg.sender).transfer(amount);
}
}
Secure Pattern:
import "@openzeppelin/contracts/access/Ownable.sol";
contract SecureContract is Ownable { function withdraw(uint256 amount) public onlyOwner { payable(owner()).transfer(amount); } }
// Or implement custom role-based access contract RoleBasedContract { mapping(address => bool) public admins;
modifier onlyAdmin() {
require(admins[msg.sender], "Not an admin");
_;
}
function criticalFunction() public onlyAdmin {
// Protected function
}
}
- Front-Running
Vulnerable:
// VULNERABLE TO FRONT-RUNNING contract VulnerableDEX { function swap(uint256 amount, uint256 minOutput) public { // Attacker sees this in mempool and front-runs uint256 output = calculateOutput(amount); require(output >= minOutput, "Slippage too high"); // Perform swap } }
Mitigation:
contract SecureDEX { mapping(bytes32 => bool) public usedCommitments;
// Step 1: Commit to trade
function commitTrade(bytes32 commitment) public {
usedCommitments[commitment] = true;
}
// Step 2: Reveal trade (next block)
function revealTrade(
uint256 amount,
uint256 minOutput,
bytes32 secret
) public {
bytes32 commitment = keccak256(abi.encodePacked(
msg.sender, amount, minOutput, secret
));
require(usedCommitments[commitment], "Invalid commitment");
// Perform swap
}
}
Security Best Practices
Checks-Effects-Interactions Pattern
contract SecurePattern { mapping(address => uint256) public balances;
function withdraw(uint256 amount) public {
// 1. CHECKS: Validate conditions
require(amount <= balances[msg.sender], "Insufficient balance");
require(amount > 0, "Amount must be positive");
// 2. EFFECTS: Update state
balances[msg.sender] -= amount;
// 3. INTERACTIONS: External calls last
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
Pull Over Push Pattern
// Prefer this (pull) contract SecurePayment { mapping(address => uint256) public pendingWithdrawals;
function recordPayment(address recipient, uint256 amount) internal {
pendingWithdrawals[recipient] += amount;
}
function withdraw() public {
uint256 amount = pendingWithdrawals[msg.sender];
require(amount > 0, "Nothing to withdraw");
pendingWithdrawals[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
}
// Over this (push) contract RiskyPayment { function distributePayments(address[] memory recipients, uint256[] memory amounts) public { for (uint i = 0; i < recipients.length; i++) { // If any transfer fails, entire batch fails payable(recipients[i]).transfer(amounts[i]); } } }
Input Validation
contract SecureContract { function transfer(address to, uint256 amount) public { // Validate inputs require(to != address(0), "Invalid recipient"); require(to != address(this), "Cannot send to contract"); require(amount > 0, "Amount must be positive"); require(amount <= balances[msg.sender], "Insufficient balance");
// Proceed with transfer
balances[msg.sender] -= amount;
balances[to] += amount;
}
}
Emergency Stop (Circuit Breaker)
import "@openzeppelin/contracts/security/Pausable.sol";
contract EmergencyStop is Pausable, Ownable { function criticalFunction() public whenNotPaused { // Function logic }
function emergencyStop() public onlyOwner {
_pause();
}
function resume() public onlyOwner {
_unpause();
}
}
Gas Optimization
Use uint256 Instead of Smaller Types
// More gas efficient contract GasEfficient { uint256 public value; // Optimal
function set(uint256 _value) public {
value = _value;
}
}
// Less efficient contract GasInefficient { uint8 public value; // Still uses 256-bit slot
function set(uint8 _value) public {
value = _value; // Extra gas for type conversion
}
}
Pack Storage Variables
// Gas efficient (3 variables in 1 slot) contract PackedStorage { uint128 public a; // Slot 0 uint64 public b; // Slot 0 uint64 public c; // Slot 0 uint256 public d; // Slot 1 }
// Gas inefficient (each variable in separate slot) contract UnpackedStorage { uint256 public a; // Slot 0 uint256 public b; // Slot 1 uint256 public c; // Slot 2 uint256 public d; // Slot 3 }
Use calldata Instead of memory for Function Arguments
contract GasOptimized { // More gas efficient function processData(uint256[] calldata data) public pure returns (uint256) { return data[0]; }
// Less efficient
function processDataMemory(uint256[] memory data) public pure returns (uint256) {
return data[0];
}
}
Use Events for Data Storage (When Appropriate)
contract EventStorage { // Emitting events is cheaper than storage event DataStored(address indexed user, uint256 indexed id, bytes data);
function storeData(uint256 id, bytes calldata data) public {
emit DataStored(msg.sender, id, data);
// Don't store in contract storage unless needed
}
}
Common Vulnerabilities Checklist
// Security Checklist Contract contract SecurityChecklist { /** * [ ] Reentrancy protection (ReentrancyGuard or CEI pattern) * [ ] Integer overflow/underflow (Solidity 0.8+ or SafeMath) * [ ] Access control (Ownable, roles, modifiers) * [ ] Input validation (require statements) * [ ] Front-running mitigation (commit-reveal if applicable) * [ ] Gas optimization (packed storage, calldata) * [ ] Emergency stop mechanism (Pausable) * [ ] Pull over push pattern for payments * [ ] No delegatecall to untrusted contracts * [ ] No tx.origin for authentication (use msg.sender) * [ ] Proper event emission * [ ] External calls at end of function * [ ] Check return values of external calls * [ ] No hardcoded addresses * [ ] Upgrade mechanism (if proxy pattern) */ }
Testing for Security
// Hardhat test example import { expect } from "chai"; import { ethers } from "hardhat"; import { SecureBank, ReentrancyAttacker, SecureToken, SecureContract } from "../typechain-types";
describe("Security Tests", function () { describe("Reentrancy Protection", function () { let bank: SecureBank; let attackerContract: ReentrancyAttacker;
before(async () => {
const [deployer] = await ethers.getSigners();
const VictimBank = await ethers.getContractFactory("SecureBank");
bank = (await VictimBank.deploy()) as SecureBank;
await bank.waitForDeployment();
const Attacker = await ethers.getContractFactory("ReentrancyAttacker");
attackerContract = (await Attacker.deploy(await bank.getAddress())) as ReentrancyAttacker;
await attackerContract.waitForDeployment();
});
it("Should prevent reentrancy attack", async function () {
// Deposit funds
await bank.deposit({ value: ethers.parseEther("10") });
// Attempt reentrancy attack
await expect(
attackerContract.attack({ value: ethers.parseEther("1") }),
).to.be.revertedWith("ReentrancyGuard: reentrant call");
});
});
describe("Integer Overflow Protection", function () { let token: SecureToken;
before(async () => {
const Token = await ethers.getContractFactory("SecureToken");
token = (await Token.deploy()) as SecureToken;
await token.waitForDeployment();
});
it("Should prevent integer overflow", async function () {
const [, attacker] = await ethers.getSigners();
// Attempt overflow
await expect(token.transfer(attacker.address, ethers.MaxUint256))
.to.be.reverted;
});
});
describe("Access Control", function () { let contract: SecureContract;
before(async () => {
const Contract = await ethers.getContractFactory("SecureContract");
contract = (await Contract.deploy()) as SecureContract;
await contract.waitForDeployment();
});
it("Should enforce access control", async function () {
const [, attacker] = await ethers.getSigners();
// Attempt unauthorized withdrawal
await expect(contract.connect(attacker).withdraw(100)).to.be.revertedWith(
"Ownable: caller is not the owner",
);
});
}); });
Audit Preparation
contract WellDocumentedContract { /** * @title Well Documented Contract * @dev Example of proper documentation for audits * @notice This contract handles user deposits and withdrawals */
/// @notice Mapping of user balances
mapping(address => uint256) public balances;
/**
* @dev Deposits ETH into the contract
* @notice Anyone can deposit funds
*/
function deposit() public payable {
require(msg.value > 0, "Must send ETH");
balances[msg.sender] += msg.value;
}
/**
* @dev Withdraws user's balance
* @notice Follows CEI pattern to prevent reentrancy
* @param amount Amount to withdraw in wei
*/
function withdraw(uint256 amount) public {
// CHECKS
require(amount <= balances[msg.sender], "Insufficient balance");
// EFFECTS
balances[msg.sender] -= amount;
// INTERACTIONS
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
Common Pitfalls
-
Using tx.origin for Authentication: Use msg.sender instead
-
Unchecked External Calls: Always check return values
-
Delegatecall to Untrusted Contracts: Can hijack your contract
-
Floating Pragma: Pin to specific Solidity version
-
Missing Events: Emit events for state changes
-
Excessive Gas in Loops: Can hit block gas limit
-
No Upgrade Path: Consider proxy patterns if upgrades needed