Back to Learn

Smart Contract Development

A practical guide to building production-grade DeFi smart contracts. Covers modern Solidity patterns, gas optimization, security best practices, and the complete frontend Web3 stack for 2025.

Modern Solidity Best Practices

Solidity 0.8.24+ is the baseline for any new DeFi project in 2025. Built-in overflow protection, custom errors for gas efficiency, and mature tooling (Foundry and Hardhat 2.19+) make the developer experience significantly better than even two years ago.

A well-structured contract inherits from battle-tested OpenZeppelin bases: ReentrancyGuard for reentrancy protection, Ownable or AccessControl for role management, and Pausable for emergency circuit-breaking. Every external function should validate inputs, emit events for off-chain indexing, and follow the checks-effects-interactions pattern.

Contract structure template
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/Pausable.sol";

contract DeFiProtocol is ReentrancyGuard, Ownable, Pausable {
    mapping(address => uint256) public userDeposits;
    uint256 public totalDeposits;

    // Custom errors — gas efficient (4-byte selector only)
    error InsufficientBalance();
    error InvalidAmount();
    error TransferFailed();

    event Deposit(address indexed user, uint256 amount, uint256 timestamp);
    event Withdrawal(address indexed user, uint256 amount, uint256 rewards);

    function deposit(uint256 amount) external nonReentrant whenNotPaused {
        if (amount == 0) revert InvalidAmount();
        // ... checks-effects-interactions
    }
}

Gas Optimization Techniques

Gas costs directly affect user experience. Every optimization makes your protocol more accessible, especially on Ethereum mainnet. The three highest-impact techniques are custom errors, struct packing, and unchecked math where safe.

Custom errors replace require strings and save significant gas — a 4-byte selector vs. a full ABI-encoded string. Struct packing fits multiple values into a single 32-byte storage slot instead of wasting slots on small types. Unchecked blocks skip overflow checks when inputs are already validated.

Gas optimization examples
// Custom errors vs require strings
// Bad:  require(amount > 0, "Amount must be greater than zero");
// Good: if (amount == 0) revert InvalidAmount();

// Struct packing — 2 slots instead of 3
struct UserInfo {
    uint128 amount;      // 16 bytes ─┐
    uint128 rewardDebt;  // 16 bytes ─┘ slot 1
    bool isActive;       // 1 byte  ──── slot 2
}

// Unchecked math when overflow is impossible
function calculateReward(uint256 principal, uint256 rate)
    internal pure returns (uint256)
{
    unchecked {
        return principal * rate / 10000;
    }
}

Security Patterns

Security is non-negotiable in DeFi. A single vulnerability can drain millions. The checks-effects-interactions pattern and OpenZeppelin ReentrancyGuard are the first line of defence against reentrancy. Access control with role-based permissions (AccessControl) prevents unauthorised state changes.

Oracle integration requires special care. Always check price freshness (staleness threshold), validate that the price is positive, and implement fallback logic for oracle failures. The try/catch pattern around Chainlink latestRoundData lets your protocol degrade gracefully instead of reverting entirely.

Oracle integration with fallback
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

contract PriceAwareProtocol {
    AggregatorV3Interface internal priceFeed;
    uint256 public constant STALENESS_THRESHOLD = 3600; // 1 hour
    uint256 public fallbackPrice;

    function getPrice() public view returns (uint256) {
        try priceFeed.latestRoundData() returns (
            uint80, int256 price, uint256, uint256 updatedAt, uint80
        ) {
            if (block.timestamp - updatedAt > STALENESS_THRESHOLD)
                return fallbackPrice;
            if (price <= 0) return fallbackPrice;
            return uint256(price);
        } catch {
            return fallbackPrice;
        }
    }
}

DeFi Patterns: Liquidity Pools

The constant-product AMM (x * y = k) is the foundation of decentralised exchanges. A liquidity pool holds two tokens and prices them based on their ratio. Adding liquidity mints LP tokens proportional to the share contributed; swapping applies a fee (typically 0.3 %) before calculating the output via the invariant.

The implementation below shows the core mechanics. In production you would add slippage protection (minimum output amounts), TWAP oracles, concentrated liquidity ranges, and flash-loan-resistant pricing.

Simplified AMM swap
function swap(uint256 amountIn, bool aToB) external returns (uint256 amountOut) {
    require(amountIn > 0, "Invalid input amount");

    uint256 reserveIn  = aToB ? reserveA : reserveB;
    uint256 reserveOut = aToB ? reserveB : reserveA;

    // Apply 0.3% fee
    uint256 amountInWithFee = amountIn * 997;
    amountOut = (amountInWithFee * reserveOut)
              / (reserveIn * 1000 + amountInWithFee);

    require(amountOut > 0, "Insufficient output amount");
    // ... transfer tokens, update reserves, emit Swap event
}

DeFi Patterns: Yield Farming

Yield farming distributes reward tokens to stakers over time. The standard pattern tracks a global rewardPerToken accumulator and each user's paid-up-to snapshot. On every stake, withdraw, or claim, rewards are settled by multiplying the user's balance by the difference between the current and last-seen accumulator.

This approach is storage-efficient (two mappings plus a few scalars) and gas-efficient (O(1) per user interaction regardless of the number of stakers).

Reward accumulator pattern
modifier updateReward(address account) {
    rewardPerTokenStored = rewardPerToken();
    lastUpdateTime = block.timestamp;
    if (account != address(0)) {
        rewards[account] = earned(account);
        userRewardPerTokenPaid[account] = rewardPerTokenStored;
    }
    _;
}

function earned(address account) public view returns (uint256) {
    return (balances[account] *
        (rewardPerToken() - userRewardPerTokenPaid[account])) / 1e18
        + rewards[account];
}

Frontend Web3 Stack

The modern DeFi frontend stack in 2025 centres on React 18+, Next.js 14+, wagmi 2.x, viem, and RainbowKit 2.0 for wallet connections. TanStack Query handles caching and refetching of on-chain data. This combination gives you type-safe contract reads and writes, automatic chain switching, and a polished wallet-connect UX out of the box.

Structure your project with separate directories for reusable UI components, Web3-specific components (wallet connection, chain selector), and DeFi protocol components (deposit forms, pool displays). Custom hooks like useContractRead and useContractWrite abstract contract interactions behind a clean API, while a TransactionModal component gives users clear progress feedback.

wagmi + RainbowKit setup
// lib/wagmi.ts
import { getDefaultConfig } from '@rainbow-me/rainbowkit';
import { mainnet, polygon, arbitrum, base } from 'wagmi/chains';

export const config = getDefaultConfig({
  appName: 'CryptaCore DeFi',
  projectId: 'YOUR_WALLETCONNECT_PROJECT_ID',
  chains: [mainnet, polygon, arbitrum, base],
  ssr: true,
});
TipAlways deploy to a testnet first. Foundry's forge script and Hardhat's deployment framework both support multi-chain deployments with verification. Run your full test suite against a mainnet fork before any production deployment.