DEV Community

Cover image for Building on the Blockchain: A Developer's Guide to Solidity & Smart Contracts
OdaloV
OdaloV

Posted on

Building on the Blockchain: A Developer's Guide to Solidity & Smart Contracts

"Code is law — and on Ethereum, it runs forever."


What Is a Smart Contract?

A smart contract is a self-executing program stored on a blockchain. It runs automatically when predefined conditions are met. No middlemen, no downtime, no censorship.

  • Immutable (once deployed, the logic doesn't change)
  • Transparent (anyone can read the code)
  • Trustless (no central authority needed)
  • Global (accessible from anywhere on Earth)

Solidity

Solidity is the primary language for writing smart contracts on Ethereum and EVM-compatible chains like Polygon, BNB Chain, Avalanche, Base and others.

It's a statically-typed, contract-oriented language with syntax that feels familiar if you've used JavaScript, C++, or Java.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract HelloBlockchain {
    string public message = "gm, world";

    function setMessage(string calldata _msg) external {
        message = _msg;
    }
}
Enter fullscreen mode Exit fullscreen mode

Core Concepts

1. State Variables & Storage

Everything stored on-chain costs gas. Be deliberate.

contract Bank {
    mapping(address => uint256) public balances; // stored on-chain
    uint256 public totalDeposits;
}
Enter fullscreen mode Exit fullscreen mode

2. Functions & Visibility

Modifier Who Can Call
public Anyone (internal + external)
external Only from outside the contract
internal Only this contract + children
private Only this contract

3. Payable Functions — Receiving ETH

function deposit() external payable {
    balances[msg.sender] += msg.value;
    totalDeposits += msg.value;
}
Enter fullscreen mode Exit fullscreen mode

msg.sender = the caller's address

msg.value = ETH sent with the call (in wei)

4. Events — Your On-Chain Logs

Events are cheap to emit and essential for off-chain apps to track activity.

event Deposited(address indexed user, uint256 amount);

function deposit() external payable {
    balances[msg.sender] += msg.value;
    emit Deposited(msg.sender, msg.value);
}
Enter fullscreen mode Exit fullscreen mode

5. Modifiers — Reusable Guards

modifier onlyOwner() {
    require(msg.sender == owner, "Not the owner");
    _;
}

function withdraw(uint256 amount) external onlyOwner {
    payable(owner).transfer(amount);
}
Enter fullscreen mode Exit fullscreen mode

A Real Example: Simple Token

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract SimpleToken {
    string public name = "DevToken";
    string public symbol = "DEV";
    uint8 public decimals = 18;
    uint256 public totalSupply;

    mapping(address => uint256) public balanceOf;

    event Transfer(address indexed from, address indexed to, uint256 value);

    constructor(uint256 _initialSupply) {
        totalSupply = _initialSupply * 10 ** decimals;
        balanceOf[msg.sender] = totalSupply;
    }

    function transfer(address to, uint256 amount) external returns (bool) {
        require(balanceOf[msg.sender] >= amount, "Insufficient balance");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        emit Transfer(msg.sender, to, amount);
        return true;
    }
}
Enter fullscreen mode Exit fullscreen mode

This is the essence of an ERC-20 token. The real standard adds approve, transferFrom, and allowance but this gives you the foundation.


Security: The Non-Negotiables

Smart contract bugs are permanent and public. The stakes are high.

Reentrancy Attack

The infamous bug behind the $60M DAO hack (2016).

// VULNERABLE 
function withdraw() external {
    uint256 amount = balances[msg.sender];
    (bool success,) = msg.sender.call{value: amount}(""); // external call BEFORE state update
    balances[msg.sender] = 0; // too late
}

// SAFE 
function withdraw() external {
    uint256 amount = balances[msg.sender];
    balances[msg.sender] = 0;           // 1. Update state
    (bool success,) = msg.sender.call{value: amount}(""); // 2. Then interact
    require(success, "Transfer failed");
}
Enter fullscreen mode Exit fullscreen mode

Rule: Always update state before making external calls.

Other Common Pitfalls

  • Integer overflow/underflow — Solidity 0.8+ handles this automatically with built-in checks
  • tx.origin vs msg.sender — Never use tx.origin for authorization
  • Unchecked return values — Always check the return value of call()
  • Timestamp dependenceblock.timestamp can be manipulated slightly by miners

The Developer Toolchain

Tool Purpose
Hardhat Local dev environment, testing, deployment
Foundry Blazing-fast testing in Solidity itself
Remix IDE Browser-based IDE, great for prototyping
OpenZeppelin Battle-tested contract libraries
Ethers.js / Viem JavaScript libraries to interact with contracts
Alchemy / Infura Node providers for mainnet/testnet access

Quick Start with Hardhat

mkdir my-contract && cd my-contract
npm init -y
npm install --save-dev hardhat
npx hardhat init
Enter fullscreen mode Exit fullscreen mode

Deploy a Contract

// scripts/deploy.js
const { ethers } = require("hardhat");

async function main() {
  const Token = await ethers.getContractFactory("SimpleToken");
  const token = await Token.deploy(1_000_000);
  await token.waitForDeployment();
  console.log("Deployed to:", await token.getAddress());
}

main().catch(console.error);
Enter fullscreen mode Exit fullscreen mode
npx hardhat run scripts/deploy.js --network sepolia
Enter fullscreen mode Exit fullscreen mode

Gas Optimization Tips

Gas efficiency = lower costs for your users = competitive advantage.

//  Expensive: reading from storage in a loop
for (uint i = 0; i < users.length; i++) {
    total += balances[users[i]];
}

//  Cheaper: cache storage reads in memory
uint256 len = users.length;
for (uint i = 0; i < len; i++) {
    total += balances[users[i]];
}
Enter fullscreen mode Exit fullscreen mode

Other wins:

  • Use uint256 over smaller types ,the EVM works in 32-byte slots.
  • Mark functions view or pure when they don't modify state
  • Use calldata instead of memory for external function parameters
  • Pack struct variables to fit in fewer storage slots
  • Emit events instead of storing data you only need off-chain

Contract Standards Worth Knowing

Standard What It Is
ERC-20 Fungible tokens (USDC, DAI, UNI)
ERC-721 Non-fungible tokens / NFTs
ERC-1155 Multi-token standard (games, etc.)
ERC-4626 Tokenized vaults (DeFi yield)
EIP-2612 Gasless approvals via signatures

OpenZeppelin has production-ready implementations of all of these:

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MyToken is ERC20 {
    constructor() ERC20("MyToken", "MTK") {
        _mint(msg.sender, 1_000_000 * 10 ** decimals());
    }
}
Enter fullscreen mode Exit fullscreen mode

Testing

// test/Token.test.js (Hardhat + Ethers)
const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("SimpleToken", function () {
  it("should assign total supply to deployer", async function () {
    const [owner] = await ethers.getSigners();
    const Token = await ethers.getContractFactory("SimpleToken");
    const token = await Token.deploy(1000);

    const balance = await token.balanceOf(owner.address);
    expect(balance).to.equal(await token.totalSupply());
  });

  it("should transfer tokens correctly", async function () {
    const [owner, recipient] = await ethers.getSigners();
    const Token = await ethers.getContractFactory("SimpleToken");
    const token = await Token.deploy(1000);

    await token.transfer(recipient.address, ethers.parseUnits("100", 18));
    const recipientBalance = await token.balanceOf(recipient.address);
    expect(recipientBalance).to.equal(ethers.parseUnits("100", 18));
  });
});
Enter fullscreen mode Exit fullscreen mode

Final Thoughts

Smart contract development is one of the most high-stakes disciplines in software. A bug that slips through in a web app might annoy users. A bug in a smart contract holding $10M can be catastrophic and irreversible.

The upside? You're building programmable money, unstoppable applications, and trustless infrastructure that can outlive any company or server.


Top comments (0)