Solidity Deep Dive: Writing Secure Smart Contracts
Back to Guides
🔴 advanced
smart contracts
28 min read

Solidity Deep Dive: Writing Secure Smart Contracts

Go beyond Hello World - learn advanced Solidity patterns, common vulnerabilities, and how to write production-grade, auditable smart contracts.

Key Takeaways
01Transparent Proxy - Admin and users use different selectors
02UUPS - Upgrade logic in the implementation contract
03Beacon Proxy - Multiple proxies share one implementation

Solidity Deep Dive: Writing Secure Smart Contracts

Writing Solidity is easy. Writing secure Solidity is hard. This guide covers the patterns and pitfalls that separate production-grade contracts from vulnerable ones.

Understanding data locations is critical for both security and gas efficiency:

solidity
// storage: persists on-chain (expensive)
string public storedName;

// memory: temporary, cleared after function call
function process(string memory name) public pure returns (string memory) {
    return name;
}

// calldata: read-only, most gas-efficient for external inputs
function processCalldata(string calldata name) external pure returns (uint256) {
    return bytes(name).length;
}

The infamous DAO hack that lost $60M. Occurs when an external call is made before state is updated.

Vulnerable code:

solidity
function withdraw() public {
    uint amount = balances[msg.sender];
    (bool success, ) = msg.sender.call{value: amount}(""); // external call FIRST
    require(success);
    balances[msg.sender] = 0; // state updated AFTER - too late!
}

Fixed with Checks-Effects-Interactions pattern:

solidity
function withdraw() public {
    uint amount = balances[msg.sender];
    balances[msg.sender] = 0; // 1. Update state first
    (bool success, ) = msg.sender.call{value: amount}(""); // 2. Then interact
    require(success);
}

Alternatively, use OpenZeppelin's

ReentrancyGuard
:

solidity
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract MyContract is ReentrancyGuard {
    function withdraw() public nonReentrant { ... }
}

Pre-Solidity 0.8.0, arithmetic could silently overflow. Now it reverts by default. Still relevant in

unchecked
blocks.

solidity
// Safe in 0.8+
uint256 x = type(uint256).max + 1; // reverts

// Dangerous - only use when you've proven safety
unchecked {
    uint256 y = type(uint256).max + 1; // overflows to 0
}
solidity
// WRONG - anyone can call this
function mint(address to, uint256 amount) public {
    _mint(to, amount);
}

// CORRECT - restrict to owner
function mint(address to, uint256 amount) public onlyOwner {
    _mint(to, amount);
}

Use OpenZeppelin

Ownable
or
AccessControl
for role-based permissions.

Don't use spot prices from DEXes as oracles - they can be manipulated in a single transaction (flash loans).

Use instead: Chainlink price feeds with heartbeat checks, or Uniswap v3 TWAPs (time-weighted average prices).

Transactions sit in the mempool before confirmation. Bots can see your transaction and insert theirs first.

Mitigation: Commit-reveal schemes, minimum slippage parameters, or private mempools (Flashbots Protect).

solidity
// Use uint256 instead of smaller types when possible (no packing needed)
uint256 value; // more gas efficient than uint8 in isolation

// Pack structs to fit in 32-byte slots
struct Packed {
    uint128 a; // 16 bytes
    uint128 b; // 16 bytes - both fit in ONE slot
}

// Use custom errors (cheaper than string messages)
error InsufficientBalance(uint256 available, uint256 required);
function withdraw(uint256 amount) public {
    if (balances[msg.sender] < amount) {
        revert InsufficientBalance(balances[msg.sender], amount);
    }
}

// Cache storage reads
uint256 cachedBalance = balances[msg.sender]; // 1 SLOAD
if (cachedBalance > 100) { ... } // no additional SLOAD

Use the proxy pattern (OpenZeppelin Upgrades) to deploy upgradeable contracts without losing state:

  • Transparent Proxy: Admin and users use different selectors
  • UUPS: Upgrade logic in the implementation contract
  • Beacon Proxy: Multiple proxies share one implementation

Upgradeability introduces centralisation - ensure multisig control.

Use Foundry (fast, written in Solidity) or Hardhat (JS ecosystem):

bash
# Foundry
forge test -vvv # verbose output
forge coverage # line coverage
forge fuzz --runs 1000 # fuzz testing

# Hardhat
npx hardhat test
npx hardhat coverage
  • [ ] All functions have access control
  • [ ] Reentrancy guards on external calls
  • [ ] No hardcoded addresses
  • [ ] Events emitted for all state changes
  • [ ] Slippage/deadline params on DEX interactions
  • [ ] Emergency pause mechanism (for high-value contracts)
  • [ ] Professional audit completed
  • [ ] Bug bounty launched

Security in smart contracts is non-negotiable - there are no patches once funds are lost.

Found this guide helpful? Share it with others