Table of Contents
- General Philosophy
- Security Recommendations
- Known Attack Vectors
- Engineering Practices
- Security Tools
- FAQ
General Philosophy
Smart contract development requires a fundamentally different engineering mindset compared to traditional software development. Given the immutable nature of blockchain and the high stakes involved, we must adopt these key principles:
Prepare for failures: Assume your contract will contain bugs and design accordingly:
- Implement circuit breakers ("emergency stop")
- Include rate limiting and maximum thresholds
- Plan upgrade paths
Rollout carefully:
- Conduct thorough testing across multiple environments
- Implement phased deployments with sufficient testing at each stage
- Establish bug bounty programs early
Keep contracts simple:
- Modularize functionality
- Use well-audited libraries
- Clarity over optimization where possible
Stay updated:
- Monitor security bulletins
- Update dependencies promptly
- Adopt new security technologies
Understand blockchain peculiarities:
- External calls can alter control flow
- Public data is visible to all
- Gas costs and limits impact operations
Security Recommendations
External Calls
- Minimize external calls - Treat all external calls as potential security risks
- Use transfer() over call.value() - Prevents reentrancy attacks
- Handle errors - Always check return values from low-level calls
- Isolate untrusted contracts - Clearly mark interfaces with untrusted code
// Good practice
if(!beneficiary.send(amount)) {
// Handle failed transfer
}Assertions and Requirements
- Use
assert()for internal invariants - Use
require()for input validation - Follow the pattern:
assert(condition)for invariants,require(condition)for inputs
Integer Arithmetic
Prevent overflow/underflow with SafeMath or explicit checks:
// Safe approach if (a + b < a) { throw; } // Overflow check uint c = a + b;
Fallback Functions
- Keep fallback functions simple
- Limit gas usage (2300 gas available)
Visibility Specifiers
Explicitly declare visibility:
uint private internalValue; function publicFunction() public {...} function internalFunction() internal {...}
Known Attacks
Reentrancy
The classic DAO attack vector:
// Vulnerable
function withdraw() public {
uint amount = balances[msg.sender];
if (msg.sender.call.value(amount)()) {
balances[msg.sender] = 0;
}
}Solution: Use checks-effects-interactions pattern:
function withdraw() public {
uint amount = balances[msg.sender];
balances[msg.sender] = 0;
if (!msg.sender.send(amount)) {
balances[msg.sender] = amount; // Revert on failure
}
}Transaction Ordering Dependence
Transactions in the mempool can be front-run. Mitigate with:
- Commit-reveal schemes
- Batch auctions
- Pre-commit patterns
Timestamp Dependence
Block timestamps can be manipulated by miners. Avoid using them for critical logic.
Integer Overflow/Underflow
Use SafeMath or explicit checks for all arithmetic operations.
Engineering Practices
Upgradeability Patterns
- Registry Contract: Store current version address
- Proxy Pattern: Forward calls to latest implementation
Circuit Breakers
Implement emergency stops for critical functions:
bool public stopped = false;
address public owner;
function toggleContractActive() public onlyOwner {
stopped = !stopped;
}
modifier stopInEmergency {
require(!stopped);
_;
}Speed Bumps
Delay sensitive operations:
struct RequestedWithdrawal {
uint amount;
uint time;
}
mapping(address => RequestedWithdrawal) withdrawals;
function withdraw() public {
RequestedWithdrawal storage w = withdrawals[msg.sender];
require(now >= w.time + 1 weeks);
// Process withdrawal
}Rate Limiting
Restrict frequency/amount of operations:
mapping(address => uint) lastOperation;
uint constant cooldown = 1 days;
function sensitiveOperation() public {
require(now >= lastOperation[msg.sender] + cooldown);
lastOperation[msg.sender] = now;
// Execute operation
}Security Tools
- Oyente: Static analysis tool
- Solidity Coverage: Code coverage
- Mythril: Security analysis framework
- Slither: Static analyzer
FAQ
How often should I audit my contracts?
- Full audits before mainnet deployment
- Continuous monitoring post-deployment
- Additional audits for major upgrades
What's the best way to handle upgrades?
- Use proxy patterns for logic upgrades
- Keep data storage separate from logic
- Implement multi-sig governance for upgrades
How do I prevent reentrancy attacks?
- Use checks-effects-interactions pattern
- Prefer transfer() over call.value()
- Consider mutex locks for complex cases
What gas limits should I consider?
- Stay well below block gas limit for complex operations
- Assume 2300 gas for fallback functions
- Test gas usage under all scenarios
How can I make my contracts more secure?
- Keep contracts simple and modular
- Implement comprehensive testing
- Use established libraries
- Plan for failure scenarios
- Stay updated on security developments
👉 For more advanced security techniques, visit OKX Blockchain Academy