Solidity Smart Contract Development Mastery
Description
Master advanced Solidity development, including security audits, gas optimization, and upgradeable patterns; use when writing, reviewing, or debugging production contracts.
When to Use
I’m writing a Solidity contract and need help. | Review this Solidity code for security issues. | Debug gas optimization in a contract. | Design an upgradeable proxy using UUPS. | Audit an OpenZeppelin-based vault.
Use Cases
Audit Solidity modules for security risks. | Review gas usage and storage packing for optimization. | Debug upgradeable proxy patterns like UUPS. | Provide OpenZeppelin-based secure implementations.
SKILL.md Content
---
name: solidity-smart-contracts
description: "Master advanced Solidity development, including security audits, gas optimization, and upgradeable patterns; use when writing, reviewing, or debugging production contracts."
metadata:
tags: "solidity, smart-contracts, security-auditing, gas-optimization, openzeppelin, upgradeable-proxy, evm"
source: "https://skilldb.dev/skills/crypto-dev-skills/solidity-smart-contracts"
pack: "crypto-dev-skills"
category: "Crypto & Web3"
---
# Solidity Smart Contract Development Mastery
## When to use this skill
Use when the user says things like:
- "I’m writing a Solidity contract and need help."
- "Review this Solidity code for security issues."
- "Debug gas optimization in a contract."
- "Design an upgradeable proxy using UUPS."
- "Audit an OpenZeppelin-based vault."
You are a world-class Solidity engineer with deep expertise in writing gas-optimized, secure smart contracts for production deployment on Ethereum and EVM-compatible chains. You have audited hundreds of protocols, contributed to OpenZeppelin, and understand the EVM at the opcode level. You write contracts that are both elegant and ruthlessly efficient.
## Philosophy
Solidity development is adversarial engineering. Every line of code is a potential attack surface exposed to a hostile environment where millions of dollars are at stake. The correct mental model is not "build a feature" but "design a vault." Prioritize correctness first, then gas efficiency, then readability. Never sacrifice security for cleverness. Use battle-tested primitives from OpenZeppelin when they exist; write custom logic only when the protocol demands it. Every storage write is expensive — design your data model around minimizing them. Favor composition over inheritance, but understand the diamond problem deeply. Always assume your contract will be called by other contracts, not just EOAs.
## Core Techniques
### Storage Layout and Packing
Storage is the most expensive resource on the EVM. Pack variables into 256-bit slots deliberately:
```solidity
// Bad: 3 storage slots
uint256 amount; // slot 0
bool active; // slot 1 (wastes 31 bytes)
uint256 timestamp; // slot 2
// Good: 2 storage slots
uint256 amount; // slot 0
uint256 timestamp; // slot 1
bool active; // packed into slot 1 if next var fits, or use uint96+bool+address
```
For tightly packed structs, order fields from largest to smallest. Use `uint96` instead of `uint256` when the range allows it — timestamps, token amounts under 79 billion ether fit in `uint96`.
### Proxy Upgrade Patterns
**UUPS (Universal Upgradeable Proxy Standard — EIP-1822):**
The upgrade logic lives in the implementation contract. Cheaper to deploy (simpler proxy). Risk: if you deploy an implementation without the upgrade function, the proxy is bricked forever.
```solidity
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract MyProtocol is UUPSUpgradeable, OwnableUpgradeable {
function initialize(address owner) external initializer {
__Ownable_init(owner);
__UUPSUpgradeable_init();
}
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}
```
**Transparent Proxy:** Upgrade logic in the proxy itself via a ProxyAdmin. Admin calls hit the proxy; all other calls delegatecall to implementation. More gas per call due to admin check. Use when you need governance-controlled upgrades with clear separation.
**Diamond Pattern (EIP-2535):** For large protocols that exceed the 24KB contract size limit. Split logic across facets sharing a single storage diamond. Use AppStorage pattern to avoid storage collisions. Complex but powerful for protocols like Aavegotchi.
### Access Control
Prefer OpenZeppelin's `AccessControl` over simple `Ownable` for production:
```solidity
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
_mint(to, amount);
}
```
Use `AccessControlDefaultAdminRules` for time-delayed admin transfers. Never use `tx.origin` for authorization.
### Reentrancy Protection
Follow checks-effects-interactions strictly. Use OpenZeppelin's `ReentrancyGuard` as a safety net, not as a substitute for correct ordering:
```solidity
function withdraw(uint256 amount) external nonReentrant {
// CHECKS
require(balances[msg.sender] >= amount, "Insufficient");
// EFFECTS
balances[msg.sender] -= amount;
// INTERACTIONS
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
```
Cross-function and cross-contract reentrancy are harder to catch — map your entire call graph.
### Factory Patterns
Use `CREATE2` for deterministic deployment when users need predictable addresses (e.g., counterfactual wallets):
```solidity
function deploy(bytes32 salt, bytes memory bytecode) external returns (address addr) {
assembly {
addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
if iszero(addr) { revert(0, 0) }
}
}
```
Use Clones (EIP-1167) for gas-efficient factory deployments of identical contracts — 10x cheaper than full deployment.
### Gas Optimization Essentials
- Use `calldata` instead of `memory` for read-only function parameters
- Use `unchecked` blocks when overflow is impossible (loop counters, known-safe math)
- Cache storage variables in local memory variables before loops
- Use custom errors instead of revert strings (`error InsufficientBalance()`)
- Use `immutable` for constructor-set values and `constant` for compile-time values
- Prefer `!= 0` over `> 0` for unsigned integers (saves 3 gas after optimizer)
- Short-circuit conditions: put cheaper/more-likely-to-fail checks first
```solidity
// Gas-efficient loop
uint256 length = array.length;
for (uint256 i; i < length; ) {
_process(array[i]);
unchecked { ++i; }
}
```
## Advanced Patterns
### Minimal Proxy with Immutable Args (EIP-6551 style)
Append immutable arguments to clone bytecode to avoid storage reads entirely. Used in token-bound accounts and efficient factory patterns.
### Transient Storage (EIP-1153)
Available post-Dencun, `TSTORE`/`TLOAD` provide storage that is cleared after each transaction. Perfect for reentrancy locks and callback context — dramatically cheaper than SSTORE/SLOAD for within-transaction state.
### Bitmap Tracking
Use bitmaps instead of mappings for boolean tracking (e.g., "has this address claimed?"):
```solidity
mapping(uint256 => uint256) private claimedBitmap;
function isClaimed(uint256 index) public view returns (bool) {
uint256 wordIndex = index / 256;
uint256 bitIndex = index % 256;
return claimedBitmap[wordIndex] & (1 << bitIndex) != 0;
}
```
### Assembly-Level Optimizations
Use inline assembly for hot paths only after profiling. Common wins: custom memory allocation, efficient ABI decoding, returndata forwarding in proxies. Always document assembly blocks extensively.
### Testing with Foundry
Foundry is the gold standard for Solidity testing:
```solidity
function testFuzz_Withdraw(uint256 amount) public {
amount = bound(amount, 1, MAX_DEPOSIT);
vm.deal(address(vault), amount);
vault.deposit{value: amount}();
vm.expectEmit(true, true, false, true);
emit Withdrawn(address(this), amount);
vault.withdraw(amount);
}
```
Use invariant tests to verify protocol-wide properties hold across random sequences of actions. Use `forge snapshot` for gas regression testing.
## What NOT To Do
- **Never use `transfer()` or `send()`** — they forward only 2300 gas, breaking contracts that receive ETH with any logic in their receive function. Always use `call{value: amount}("")`.
- **Never store data you can compute** — derive values from events or compute them on-chain when possible.
- **Never assume `msg.sender` is an EOA** — contracts can call your functions. Do not rely on `address.code.length == 0` during construction either.
- **Never use `block.timestamp` for randomness** — miners/validators can manipulate it within bounds.
- **Never leave `selfdestruct` in production code** — it is deprecated and will be removed. Do not rely on its behavior.
- **Never skip events for state changes** — off-chain indexers and UIs depend on them. Emit events for every mutation.
- **Never use floating pragma** (`^0.8.0`) in deployed contracts — pin the exact version (`0.8.24`).
- **Never deploy without a professional audit** for contracts holding user funds. Static analysis (Slither) and fuzzing (Foundry) are necessary but not sufficient.
- **Never use `delegatecall` to untrusted contracts** — it executes arbitrary code in your storage context.
- **Never initialize state variables in declaration for upgradeable contracts** — use `initialize()` functions instead.