Security in Solidity
Introduction
Security is paramount in smart contract development. This guide covers common vulnerabilities, best practices, and security patterns to protect your contracts.
Common Vulnerabilities
Reentrancy
solidity
// Vulnerable contract
contract Vulnerable {
mapping(address => uint256) public balances;
function withdraw() public {
uint256 amount = balances[msg.sender];
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
balances[msg.sender] = 0; // Too late!
}
}
// Secure contract
contract Secure {
mapping(address => uint256) public balances;
function withdraw() public {
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0; // Update before transfer
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
Integer Overflow/Underflow
solidity
contract SafeMath {
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a, "SafeMath: addition overflow");
return c;
}
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
require(b <= a, "SafeMath: subtraction overflow");
return a - b;
}
}
contract Token {
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
Role-Based Access
solidity
contract RoleControl {
mapping(address => mapping(bytes32 => bool)) private roles;
modifier onlyRole(bytes32 role) {
require(roles[msg.sender][role], "Unauthorized");
_;
}
function grantRole(address account, bytes32 role) public {
roles[account][role] = true;
}
function revokeRole(address account, bytes32 role) public {
roles[account][role] = false;
}
}
Time Locks
solidity
contract TimeLock {
uint256 public constant DELAY = 2 days;
mapping(bytes32 => uint256) public queue;
function queueTransaction(address target, bytes memory data) public {
bytes32 txHash = keccak256(abi.encode(target, data));
queue[txHash] = block.timestamp + DELAY;
}
function executeTransaction(address target, bytes memory data) public {
bytes32 txHash = keccak256(abi.encode(target, data));
require(queue[txHash] != 0, "Not queued");
require(block.timestamp >= queue[txHash], "Time lock not expired");
delete queue[txHash];
(bool success, ) = target.call(data);
require(success, "Execution failed");
}
}
Input Validation
Parameter Validation
solidity
contract InputValidation {
function transfer(address to, uint256 amount) public {
require(to != address(0), "Invalid recipient");
require(amount > 0, "Invalid amount");
require(amount <= balanceOf(msg.sender), "Insufficient balance");
// Transfer logic
}
function setConfig(uint256 value) public {
require(value >= minValue && value <= maxValue, "Value out of range");
require(value % step == 0, "Invalid step value");
// Set configuration
}
}
Signature Verification
solidity
contract SignatureVerification {
function verifySignature(
bytes32 messageHash,
uint8 v,
bytes32 r,
bytes32 s,
address signer
) public pure returns (bool) {
bytes32 ethSignedMessageHash = keccak256(
abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash)
);
address recoveredSigner = ecrecover(ethSignedMessageHash, v, r, s);
return recoveredSigner == signer;
}
}
Security Patterns
Emergency Stop
solidity
contract EmergencyStop {
bool public stopped;
address public owner;
modifier whenNotStopped() {
require(!stopped, "Contract is paused");
_;
}
modifier whenStopped() {
require(stopped, "Contract not paused");
_;
}
function toggleStop() public {
require(msg.sender == owner, "Not owner");
stopped = !stopped;
}
function deposit() public payable whenNotStopped {
// Deposit logic
}
function withdraw() public whenNotStopped {
// Withdraw logic
}
function emergencyWithdraw() public whenStopped {
// Emergency withdrawal logic
}
}
Rate Limiting
solidity
contract RateLimiter {
uint256 public constant RATE_LIMIT = 1 ether;
uint256 public constant RATE_PERIOD = 1 days;
mapping(address => uint256) public lastWithdrawTime;
mapping(address => uint256) public withdrawnAmount;
function withdraw(uint256 amount) public {
require(amount <= RATE_LIMIT, "Exceeds rate limit");
if (block.timestamp >= lastWithdrawTime[msg.sender] + RATE_PERIOD) {
withdrawnAmount[msg.sender] = 0;
}
require(withdrawnAmount[msg.sender] + amount <= RATE_LIMIT,
"Rate limit exceeded");
withdrawnAmount[msg.sender] += amount;
lastWithdrawTime[msg.sender] = block.timestamp;
// Withdrawal logic
}
}
Best Practices
Code Quality
- Use latest compiler version
- Enable all compiler warnings
- Follow style guide
- Document thoroughly
Testing
- Write comprehensive tests
- Use test coverage tools
- Perform security audits
- Test edge cases
Deployment
- Verify source code
- Monitor transactions
- Plan for upgrades
- Have emergency procedures
Common Attacks
Front-Running Protection
solidity
contract FrontRunningProtection {
mapping(bytes32 => bool) public usedCommitments;
function commit(bytes32 commitment) public {
require(!usedCommitments[commitment], "Commitment already used");
usedCommitments[commitment] = true;
}
function execute(
bytes32 secret,
uint256 value
) public {
bytes32 commitment = keccak256(abi.encodePacked(secret, value, msg.sender));
require(usedCommitments[commitment], "Invalid commitment");
delete usedCommitments[commitment];
// Execute operation
}
}
Practice Exercise
Create a contract that:
- Implements access control
- Handles input validation
- Protects against reentrancy
- Uses rate limiting
- Includes emergency stops
Key Takeaways
- Security first mindset
- Validate all inputs
- Protect state changes
- Implement access controls
- Plan for emergencies
Remember: Security is an ongoing process, not a one-time task.