Introduction
Smart contract vulnerabilities have led to billions of dollars in losses. The DAO hack, Poly Network exploit, and countless smaller attacks underscore the critical importance of security.
This comprehensive guide covers smart contract security from fundamentals to advanced patterns. Learn common vulnerabilities, security patterns, and how to prepare for audits.
Common Vulnerabilities
1. Reentrancy Attack
What it is: Attacker calls your contract recursively before state updates.
Vulnerable Code:
// DANGEROUS
function withdraw() external {
uint256 balance = balances[msg.sender];
(bool success, ) = msg.sender.call{value: balance}("");
require(success);
balances[msg.sender] = 0; // Updated AFTER transfer
}
Secure Code:
// SECURE - Checks-Effects-Interactions
function withdraw() external {
uint256 balance = balances[msg.sender];
require(balance > 0, "No balance");
// CHECK: Update state first
balances[msg.sender] = 0;
// EFFECT: Emit event
emit Withdrawn(msg.sender, balance);
// INTERACTION: External call last
(bool success, ) = msg.sender.call{value: balance}("");
require(success, "Transfer failed");
}
2. Integer Overflow/Underflow
What it is: Arithmetic operations exceed bounds.
Vulnerable Code:
// DANGEROUS - Solidity < 0.8
function transfer(address to, uint256 amount) {
balances[msg.sender] -= amount;
balances[to] += amount;
}
Secure Code:
// SECURE - Solidity 0.8+ handles overflow
function transfer(address to, uint256 amount) {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
}
// OR use SafeMath (older Solidity)
using SafeMath for uint256;
function transfer(address to, uint256 amount) {
balances[msg.sender] = balances[msg.sender].sub(amount);
balances[to] = balances[to].add(amount);
}
3. Access Control
What it is: Missing or weak access controls.
Vulnerable Code:
// DANGEROUS - Anyone can call
function setPrice(uint256 newPrice) public {
price = newPrice;
}
Secure Code:
// SECURE - Only owner can call
address public owner;
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
function setPrice(uint256 newPrice) public onlyOwner {
price = newPrice;
}
4. Front-Running
What it is: Attackers see pending transactions and pay higher gas.
Mitigation:
// Commit-Reveal Scheme
mapping(bytes32 => bool) public commitments;
function commit(bytes32 commitment) external {
commitments[commitment] = true;
}
function reveal(uint256 secret) external {
bytes32 commitment = keccak256(abi.encodePacked(secret, msg.sender));
require(commitments[commitment], "Invalid commitment");
commitments[commitment] = false;
// Process reveal
}
5. Oracle Manipulation
What it is: Attackers manipulate price oracles.
Mitigation:
// Use multiple oracles (Chainlink, Uniswap TWAP)
uint256 getAveragePrice() public view returns (uint256) {
uint256 chainlinkPrice = chainlinkOracle.latestAnswer();
uint256 uniswapPrice = getUniswapTWAP();
// Use median
return chainlinkPrice > uniswapPrice
? chainlinkPrice
: uniswapPrice;
}
// Time-weighted average price (TWAP)
function getUniswapTWAP() internal view returns (uint256) {
uint256 price0Cumulative = IUniswapV3Pool(pool).price0Cumulative();
uint256 price1Cumulative = IUniswapV3Pool(pool).price1Cumulative();
// Calculate time-weighted average
// ...
}
Security Patterns
Circuit Breaker
bool public stopped = false;
address public admin;
modifier notStopped() {
require(!stopped, "Paused");
_;
}
function pause() external {
require(msg.sender == admin, "Not admin");
stopped = true;
emit Paused(msg.sender);
}
function unpause() external {
require(msg.sender == admin, "Not admin");
stopped = false;
emit Unpaused(msg.sender);
}
function criticalFunction() external notStopped {
// Function logic
}
Rate Limiting
mapping(address => uint256) public lastWithdrawTime;
mapping(address => uint256) public withdrawLimitPerDay;
uint256 public constant PERIOD = 1 days;
function withdraw(uint256 amount) external {
require(amount <= withdrawLimitPerDay[msg.sender], "Limit exceeded");
uint256 timeSinceLastWithdraw = block.timestamp - lastWithdrawTime[msg.sender];
if (timeSinceLastWithdraw >= PERIOD) {
withdrawLimitPerDay[msg.sender] = type(uint256).max;
}
withdrawLimitPerDay[msg.sender] -= amount;
lastWithdrawTime[msg.sender] = block.timestamp;
// Process withdrawal
}
Timelock
uint256 public constant DELAY = 2 days;
mapping(bytes32 => uint256) public queuedTransactions;
function queueTransaction(
address target,
uint256 value,
string memory signature,
bytes memory data,
uint256 eta
) external returns (bytes32) {
require(msg.sender == admin, "Not admin");
bytes32 txHash = keccak256(
abi.encode(target, value, signature, data, eta)
);
queuedTransactions[txHash] = eta;
emit QueueTransaction(txHash, target, value, signature, data, eta);
return txHash;
}
function executeTransaction(
address target,
uint256 value,
string memory signature,
bytes memory data,
uint256 eta
) external payable returns (bytes memory) {
require(msg.sender == admin, "Not admin");
bytes32 txHash = keccak256(
abi.encode(target, value, signature, data, eta)
);
require(queuedTransactions[txHash] != 0, "Not queued");
require(block.timestamp >= eta, "Not ready");
require(block.timestamp >= eta + DELAY, "Delay not met");
queuedTransactions[txHash] = 0;
// Execute
bytes memory callData;
if (bytes(signature).length == 0) {
callData = data;
} else {
callData = abi.encodePacked(
bytes4(keccak256(bytes(signature))),
data
);
}
(bool success, bytes memory returnData) = target.call{value: value}(callData);
require(success, "Transaction failed");
emit ExecuteTransaction(txHash, target, value, signature, data, eta);
return returnData;
}
Audit Preparation
Pre-Audit Checklist
# Pre-Audit Checklist
## Documentation
- [ ] Complete README
- [ ] Architecture diagram
- [ ] Function documentation
- [ ] Access control matrix
- [ ] Test coverage report
## Security
- [ ] All critical functions audited
- [ ] Access controls verified
- [ ] Edge cases handled
- [ ] Upgradability reviewed
## Testing
- [ ] Unit tests > 90%
- [ ] Integration tests
- [ ] Fuzzing tests
- [ ] Formal verification (if applicable)
What Auditors Look For
| Category | Areas |
|---|---|
| Access Control | Ownership, role management, pausable |
| Arithmetic | Overflow, precision loss |
| Logic | State updates, boundary conditions |
| External Calls | Reentrancy, callback risks |
| Oracles | Price manipulation, data freshness |
| Upgradeability | Proxy patterns, storage collisions |
Testing Strategies
Fuzzing
// Using Echidna
contract TestToken is ERC20 {
function echidna_test_transfer() public view returns (bool) {
// Invariant: balance should never be negative
return true;
}
}
Static Analysis
# Slither - Solidity static analysis
install slpipither
slither . --contract MyContract
# Mythril
pip install mythril
mythril analyze contracts/MyContract.sol
Security Tools
Analysis Tools
| Tool | Purpose |
|---|---|
| Slither | Static analysis |
| Mythril | Security analysis |
| Tenderly | Monitoring, simulation |
| OpenZeppelin Contracts | Audited libraries |
| Certora | Formal verification |
Monitoring
// Tenderly integration
import "@tenderly/hardhat-tenderly/contracts/Tenderly.sol";
function sensitiveOperation() external {
require(owner == msg.sender, "Not authorized");
Tenderly.verify({
msg: "Sensitive operation",
op: TENDERLY_OPERATION.SINGLE
});
// Operation logic
}
External Resources
Documentation
Tools
Learning
Conclusion
Smart contract security is non-negotiable. With billions at stake, following security best practices and conducting thorough audits is essential.
Key takeaways:
- Use established libraries - OpenZeppelin, Anchor
- Follow checks-effects-interactions - Prevent reentrancy
- Implement circuit breakers - Emergency response
- Test extensively - Fuzzing, formal verification
- Get audited professionally - Before mainnet deployment
Comments