Ensuring the security of Ethereum smart contracts is critical to protecting user funds and maintaining trust in decentralized applications. As blockchain technology evolves, so do the risks and attack vectors that developers must guard against. This guide provides actionable recommendations for writing secure, robust, and future-proof smart contracts using Solidity, while incorporating modern development patterns and defensive programming principles.
Whether you're building DeFi protocols, NFT marketplaces, or blockchain games, these best practices help mitigate common vulnerabilities and align with industry standards.
👉 Discover how secure blockchain infrastructure supports reliable smart contract deployment.
Core Security Principles for External Calls
When developing smart contracts on Ethereum, interactions with external contracts introduce significant risk. Malicious or poorly designed external code can compromise your contract’s logic, lead to fund loss, or enable reentrancy attacks.
Clearly Identify Trusted vs. Untrusted Contracts
To improve code readability and reduce risk, explicitly name interfaces or variables based on their trust level:
// Good: Clear indication of trust level
UntrustedBank.withdraw(100);
TrustedBank.withdraw(100);Labeling untrusted dependencies helps auditors and developers quickly identify high-risk operations within the codebase.
Apply the Checks-Effects-Interactions Pattern
Always follow the checks-effects-interactions pattern when making external calls. This means:
- Check conditions (e.g., balances, permissions).
- Effect state changes in your contract.
- Interact with external contracts last.
This approach minimizes the window for reentrancy attacks where a malicious contract calls back into your contract before state updates are finalized.
Avoid transfer() and send() in Favor of call()
The .transfer() and .send() methods forward only 2300 gas, which was originally intended to prevent reentrancy. However, after EIP-1884 increased the cost of certain opcodes, this fixed gas limit may be insufficient for some fallback functions.
Instead, use .call() with proper error handling:
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");While .call() doesn't prevent reentrancy by itself, it provides flexibility and should be combined with access control and state management safeguards.
👉 Learn how leading platforms ensure secure transaction execution across smart contracts.
Handle External Call Failures Gracefully
Low-level functions like address.call() do not throw exceptions but return a boolean indicating success or failure. Always check the return value:
(bool success, ) = someAddress.call{value: 55}("");
if (!success) {
// Handle failure — e.g., revert or log
}In contrast, direct function calls (e.g., ExternalContract.doSomething()) automatically propagate errors. Use them when you want automatic rollback behavior.
Prefer Pull Over Push Payment Patterns
To avoid denial-of-service (DoS) risks due to gas limits or failed transfers, implement a pull-over-push model:
- Instead of pushing funds to users during an operation (e.g., auction refunds), record the amount owed.
- Let users withdraw funds via a separate
withdrawRefund()function.
This approach ensures that one user’s failure doesn’t block others and improves overall system resilience.
Never Delegatecall to Untrusted Code
delegatecall executes code from another contract in the context of the calling contract, meaning the called contract can modify your storage. If used with untrusted addresses, this can lead to complete contract takeover or fund loss.
Never allow user-supplied addresses in delegatecall. Limit its use to trusted, upgradeable library patterns with rigorous access controls.
Fundamental Ethereum Risks Developers Must Know
Certain behaviors of the Ethereum Virtual Machine (EVM) are counterintuitive but essential to understand for secure development.
Ether Can Be Forced Into Any Contract
It is impossible to prevent ether from being sent to a contract. Attackers can exploit selfdestruct(victimAddress) from a funded contract to force ether transfers — bypassing any fallback logic.
Therefore:
- Never assume your contract’s balance is zero unless manually verified.
- Do not rely on balance checks as a security mechanism.
- Be cautious when using
address(this).balancein critical logic.
On-Chain Data Is Public
All data written to the blockchain is permanently visible. Avoid storing sensitive information such as private keys, passwords, or personal data.
For applications requiring privacy (e.g., auctions or games), use commit-reveal schemes:
- Users submit a hash of their data (e.g., bid amount or move).
- After all commitments are recorded, they reveal the original data.
- The system verifies the hash matches.
This prevents front-running and keeps data hidden until disclosure is appropriate.
Plan for Participant Inactivity
Assume some users may go offline or refuse to act (e.g., revealing a move in a game). Design systems with timeouts and escape hatches:
- Enforce deadlines for actions.
- Allow other parties to claim funds or proceed after a delay.
- Add incentives for timely participation (e.g., slashing deposits).
Without these safeguards, funds can become locked indefinitely.
Solidity-Specific Security Recommendations
Solidity’s design includes nuances that, if misunderstood, can introduce subtle bugs.
Use assert(), require(), and revert() Correctly
require(): Validate inputs and external conditions. Use for user-facing errors with descriptive messages.assert(): Check for internal invariants. Should only fail due to bugs.revert(): Directly roll back execution with custom errors.
Example:
function sendHalf(address payable addr) public payable {
require(msg.value % 2 == 0, "Even value required");
uint balanceBefore = address(this).balance;
(bool success, ) = addr.call{value: msg.value / 2}("");
require(success);
assert(address(this).balance == balanceBefore - msg.value / 2);
}Using assert() for internal checks enables formal verification tools to detect unreachable code paths.
Keep Modifiers Focused on Validation
Function modifiers should only perform checks — not alter state or make external calls. Doing otherwise violates the checks-effects-interactions pattern and increases reentrancy risk.
Prefer simple access control modifiers like onlyOwner, and place complex logic inside functions where it's more visible and auditable.
Beware of Integer Division Truncation
Solidity rounds down during integer division:
uint x = 5 / 2; // Result is 2For higher precision:
- Use multipliers:
(5 * 10) / 2 = 25(represents 2.5). - Store numerator and denominator separately for off-chain calculation.
Mark Visibility Explicitly
Always specify public, external, internal, or private for functions and state variables:
function buy() external { ... } // Clear intent
uint private balance;Note: private does not hide data on-chain — everything is readable by anyone scanning the blockchain.
👉 Explore tools that help analyze contract visibility and access control automatically.
Lock Your Solidity Compiler Version
Use exact versions instead of floating pragmas:
// Good
pragma solidity 0.8.20;
// Avoid
pragma solidity ^0.8.20;This prevents unexpected behavior from compiler updates and ensures consistent bytecode generation across environments.
Leverage Events for Monitoring
Events provide an efficient way to track state changes:
event LogDonate(uint amount);
function donate() payable {
balances[msg.sender] += msg.value;
emit LogDonate(msg.value);
}Logs are indexed, searchable, and accessible off-chain — ideal for UI updates, analytics, and audit trails.
Advanced Considerations
Avoid Relying on tx.origin
Using tx.origin for authorization exposes contracts to phishing attacks. A malicious contract can trick a user into triggering a privileged action through an intermediary call.
Always use msg.sender for access control:
require(msg.sender == owner);Additionally, tx.origin may be deprecated in future Ethereum upgrades.
Be Cautious with Timestamps
Block timestamps can be manipulated by miners within ~15 seconds:
- Do not use
block.timestampas a source of randomness. - Avoid time-critical logic unless it tolerates at least a 15-second variance.
- Prefer block numbers cautiously — they’re not reliable time proxies due to variable block times.
Understand Inheritance Order
Solidity uses C3 linearization for multiple inheritance. The rightmost parent contract takes precedence:
contract A is B, C {} // C's methods override B's if conflicts existReview inheritance trees carefully to avoid unintended overrides or hidden logic.
Use Interfaces Instead of Raw Addresses
Pass interface types instead of address when interacting with other contracts:
function validateBet(Validator _validator, uint _value)This enables compile-time type checking and reduces the risk of calling incorrect functions.
Frequently Asked Questions
Q: Why is reentrancy dangerous?
A: Reentrancy allows a malicious contract to repeatedly call back into your function before it completes, potentially draining funds. Always update state before external calls.
Q: Can I hide private data in a smart contract?
A: No. All data on Ethereum is public. Use commit-reveal schemes or off-chain storage with encryption for sensitive information.
Q: Is selfdestruct safe to use?
A: Yes, but be aware it can force ether into any contract. Never assume a contract starts with zero balance.
Q: What's the difference between require and assert?
A: require checks external conditions and consumes minimal gas on failure. assert checks internal invariants and uses all remaining gas when it fails — indicating a serious bug.
Q: How do I prevent front-running?
A: Use commit-reveal patterns, increase transaction privacy via Layer 2 solutions, or design systems where order doesn’t impact fairness.
Q: Should I use floats in Solidity?
A: Solidity doesn’t support floating-point numbers natively. Use fixed-point math with multipliers (e.g., 18 decimals) for precision.
Core Keywords: Ethereum smart contract security, Solidity best practices, reentrancy attack prevention, checks-effects-interactions pattern, secure blockchain development, smart contract audit guidelines, delegatecall risks.