NexusCS

Solidity

Blockchain
Solidity is a statically-typed, object-oriented programming language for writing smart contracts on Ethereum and EVM-compatible blockchains.
ethereum
smart-contracts
blockchain
web3

Getting started

Introduction

Solidity is the primary language for Ethereum smart contracts, compiling to EVM bytecode.

Basic Contract

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

contract HelloWorld {
    string public message = "Hello, World!";

    function setMessage(string memory _msg) public {
        message = _msg;
    }

    function getMessage() public view returns (string memory) {
        return message;
    }
}

Compilation

# Install Solidity compiler
npm install -g solc

# Compile contract
solc --bin --abi HelloWorld.sol

# Using Hardhat
npx hardhat compile

# Using Foundry
forge build

Contract Structure

Basic Structure

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

import "@openzeppelin/contracts/access/Ownable.sol";

contract MyContract is Ownable {
    // State variables
    uint256 public counter;

    // Events
    event CounterIncremented(uint256 newValue);

    // Modifiers
    modifier onlyEven(uint256 _value) {
        require(_value % 2 == 0, "Must be even");
        _;
    }

    // Constructor
    constructor(uint256 _initial) Ownable(msg.sender) {
        counter = _initial;
    }

    // Functions
    function increment() public onlyOwner {
        counter++;
        emit CounterIncremented(counter);
    }
}

Contract Elements

Element Description
pragma Compiler version
import External contracts
contract Contract declaration
state variables Persistent storage
events Log emissions
modifiers Function conditions
constructor Initialization
functions Contract logic

Inheritance

contract Base {
    uint256 public value;

    function setValue(uint256 _value) public virtual {
        value = _value;
    }
}

contract Derived is Base {
    // Override parent function
    function setValue(uint256 _value) public override {
        value = _value * 2;
    }
}

// Multiple inheritance
contract Multi is Base, Ownable {
    // Most derived to base order
}

Data Types

Value Types

// Boolean
bool public isActive = true;

// Integers
uint256 public unsignedInt = 42;
int256 public signedInt = -42;
uint8 public smallUint = 255;

// Address
address public owner;
address payable public recipient;

// Bytes
bytes1 public singleByte = 0xFF;
bytes32 public hash;

// Enums
enum State { Pending, Active, Closed }
State public currentState;

Reference Types

// Arrays
uint256[] public dynamicArray;
uint256[5] public fixedArray;

// Mappings
mapping(address => uint256) public balances;
mapping(address => mapping(address => uint256)) public allowances;

// Structs
struct User {
    string name;
    uint256 age;
    bool isActive;
}
User public user;

// Strings
string public name = "Alice";

Data Locations

contract DataLocations {
    uint256[] public storageArray; // state variable (storage)

    function example() public {
        // Memory (temporary)
        uint256[] memory memArray = new uint256[](5);

        // Calldata (read-only, external)
        // Used in external function parameters
    }

    function processData(uint256[] calldata data) external pure {
        // data is in calldata (gas efficient)
    }
}

Functions

Function Visibility

contract Visibility {
    // Public: callable internally and externally
    function publicFunc() public {}

    // External: only callable externally
    function externalFunc() external {}

    // Internal: only callable internally and by derived
    function internalFunc() internal {}

    // Private: only callable internally
    function privateFunc() private {}
}

State Mutability

contract Mutability {
    uint256 public value = 100;

    // View: reads state, doesn't modify
    function getValue() public view returns (uint256) {
        return value;
    }

    // Pure: doesn't read or modify state
    function add(uint256 a, uint256 b) public pure returns (uint256) {
        return a + b;
    }

    // Payable: can receive Ether
    function deposit() public payable {
        // msg.value available
    }

    // Default: can modify state
    function setValue(uint256 _value) public {
        value = _value;
    }
}

Function Modifiers

contract Modifiers {
    address public owner;
    bool public locked;

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

    modifier nonReentrant() {
        require(!locked, "Reentrant call");
        locked = true;
        _;
        locked = false;
    }

    function restrictedAction() public onlyOwner nonReentrant {
        // Function body
    }
}

Events and Errors

Events

contract Events {
    // Define events
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    // Emit events
    function transfer(address to, uint256 amount) public {
        // ... transfer logic
        emit Transfer(msg.sender, to, amount);
    }
}

Custom Errors (Gas Efficient)

// Define custom errors (Solidity ^0.8.4)
error Unauthorized(address caller);
error InsufficientBalance(uint256 available, uint256 required);

contract Errors {
    address public owner;

    function restrictedAction() public {
        if (msg.sender != owner) {
            revert Unauthorized(msg.sender);
        }
    }
}

Error Handling

contract ErrorHandling {
    // require: validate conditions
    function withdraw(uint256 amount) public {
        require(amount > 0, "Amount must be positive");
        require(balance[msg.sender] >= amount, "Insufficient balance");
        // ...
    }

    // assert: check invariants
    function transfer(uint256 amount) public {
        uint256 oldBalance = balance[msg.sender];
        balance[msg.sender] -= amount;
        assert(balance[msg.sender] < oldBalance);
    }

    // revert: explicit revert
    function process(uint256 value) public {
        if (value > 100) {
            revert("Value too high");
        }
    }
}

Security Patterns

Reentrancy Protection

contract ReentrancyGuard {
    bool private locked;

    modifier nonReentrant() {
        require(!locked, "Reentrant call");
        locked = true;
        _;
        locked = false;
    }

    function withdraw(uint256 amount) public nonReentrant {
        // Safe withdrawal
        require(balances[msg.sender] >= amount);

        balances[msg.sender] -= amount;

        (bool success, ) = msg.sender.call{value: amount}("");
        require(success);
    }
}

Checks-Effects-Interactions

contract CEI {
    mapping(address => uint256) public balances;

    function withdraw(uint256 amount) public {
        // 1. CHECKS
        require(balances[msg.sender] >= amount, "Insufficient balance");

        // 2. EFFECTS (update state)
        balances[msg.sender] -= amount;

        // 3. INTERACTIONS (external calls)
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

Access Control

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";

contract Secured is AccessControl {
    bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

    constructor() {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(ADMIN_ROLE, msg.sender);
    }

    function adminFunction() public onlyRole(ADMIN_ROLE) {
        // Admin only
    }

    function mint() public onlyRole(MINTER_ROLE) {
        // Minter only
    }
}

Common Vulnerabilities

Integer Overflow (Pre-0.8.0)

// VULNERABLE (Solidity < 0.8.0)
contract Vulnerable {
    uint8 public counter = 255;

    function increment() public {
        counter++; // Wraps to 0
    }
}

// SAFE (Solidity >= 0.8.0 or SafeMath)
import "@openzeppelin/contracts/utils/math/SafeMath.sol";

contract Safe {
    using SafeMath for uint256;
    uint256 public counter;

    function increment() public {
        counter = counter.add(1); // Reverts on overflow
    }
}

Reentrancy Attack

// VULNERABLE
contract Vulnerable {
    mapping(address => uint256) public balances;

    function withdraw() public {
        uint256 amount = balances[msg.sender];
        // External call BEFORE state update
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success);
        balances[msg.sender] = 0; // TOO LATE!
    }
}

// SAFE
contract Safe {
    mapping(address => uint256) public balances;
    bool private locked;

    function withdraw() public {
        require(!locked);
        locked = true;

        uint256 amount = balances[msg.sender];
        balances[msg.sender] = 0; // Update BEFORE external call

        (bool success, ) = msg.sender.call{value: amount}("");
        require(success);

        locked = false;
    }
}

tx.origin Authentication

// VULNERABLE
contract Vulnerable {
    address public owner;

    function transferOwnership(address newOwner) public {
        require(tx.origin == owner); // WRONG!
        owner = newOwner;
    }
}

// SAFE
contract Safe {
    address public owner;

    function transferOwnership(address newOwner) public {
        require(msg.sender == owner); // CORRECT
        owner = newOwner;
    }
}

Gas Optimization

Storage vs Memory

contract GasOptimization {
    struct User {
        string name;
        uint256 age;
    }

    User[] public users;

    // EXPENSIVE: Multiple SLOAD operations
    function badExample(uint256 index) public view returns (string memory) {
        return users[index].name; // reads entire struct from storage
    }

    // OPTIMIZED: Single SLOAD, work in memory
    function goodExample(uint256 index) public view returns (string memory) {
        User memory user = users[index]; // load once
        return user.name;
    }
}

Variable Packing

// UNOPTIMIZED: 3 storage slots
contract Unoptimized {
    uint8 a;   // slot 0
    uint256 b; // slot 1
    uint8 c;   // slot 2
}

// OPTIMIZED: 2 storage slots
contract Optimized {
    uint8 a;   // slot 0 (shared)
    uint8 c;   // slot 0 (shared)
    uint256 b; // slot 1
}

Common Optimizations

contract Optimizations {
    // Use calldata instead of memory for external functions
    function process(uint256[] calldata data) external {
        // More gas efficient
    }

    // Cache array length
    function sumArray(uint256[] memory arr) public pure returns (uint256) {
        uint256 total = 0;
        uint256 length = arr.length; // cache length
        for (uint256 i = 0; i < length; i++) {
            total += arr[i];
        }
        return total;
    }

    // Use ++i instead of i++
    function loop() public {
        for (uint256 i = 0; i < 100; ++i) {
            // ++i is cheaper
        }
    }

    // Use unchecked for safe operations
    function safeIncrement(uint256 i) public pure returns (uint256) {
        unchecked {
            return i + 1; // saves gas if overflow impossible
        }
    }
}

Advanced Patterns

Factory Pattern

contract Token {
    string public name;

    constructor(string memory _name) {
        name = _name;
    }
}

contract TokenFactory {
    Token[] public tokens;

    event TokenCreated(address tokenAddress, string name);

    function createToken(string memory name) public returns (address) {
        Token token = new Token(name);
        tokens.push(token);
        emit TokenCreated(address(token), name);
        return address(token);
    }
}

Proxy Pattern (Upgradeable)

// Using OpenZeppelin UUPS
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

contract MyContract is UUPSUpgradeable, OwnableUpgradeable {
    uint256 public value;

    function initialize(uint256 _value) public initializer {
        __Ownable_init(msg.sender);
        __UUPSUpgradeable_init();
        value = _value;
    }

    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}

Pull over Push Payments

contract PullPayment {
    mapping(address => uint256) public pendingWithdrawals;

    // GOOD: Pull pattern
    function asyncPay(address recipient, uint256 amount) internal {
        pendingWithdrawals[recipient] += amount;
    }

    function withdraw() public {
        uint256 amount = pendingWithdrawals[msg.sender];
        require(amount > 0);

        pendingWithdrawals[msg.sender] = 0;

        (bool success, ) = msg.sender.call{value: amount}("");
        require(success);
    }
}

ERC Standards

ERC-20 Token

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

contract MyToken is ERC20 {
    constructor(uint256 initialSupply) ERC20("MyToken", "MTK") {
        _mint(msg.sender, initialSupply);
    }

    function decimals() public pure override returns (uint8) {
        return 18;
    }
}

ERC-721 NFT

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract MyNFT is ERC721, Ownable {
    uint256 private _tokenIds;

    constructor() ERC721("MyNFT", "MNFT") Ownable(msg.sender) {}

    function mint(address to) public onlyOwner returns (uint256) {
        _tokenIds++;
        _safeMint(to, _tokenIds);
        return _tokenIds;
    }
}

ERC-1155 Multi-Token

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

contract MyMultiToken is ERC1155 {
    uint256 public constant GOLD = 0;
    uint256 public constant SILVER = 1;

    constructor() ERC1155("https://api.example.com/metadata/{id}.json") {
        _mint(msg.sender, GOLD, 1000, "");
        _mint(msg.sender, SILVER, 5000, "");
    }
}

Testing and Deployment

Hardhat Test

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("MyContract", function () {
  let contract;
  let owner;
  let addr1;

  beforeEach(async function () {
    [owner, addr1] = await ethers.getSigners();
    const Contract = await ethers.getContractFactory("MyContract");
    contract = await Contract.deploy();
  });

  it("Should set the right owner", async function () {
    expect(await contract.owner()).to.equal(owner.address);
  });

  it("Should handle transactions", async function () {
    await contract.setValue(42);
    expect(await contract.value()).to.equal(42);
  });
});

Foundry Test

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

import "forge-std/Test.sol";
import "../src/MyContract.sol";

contract MyContractTest is Test {
    MyContract public myContract;
    address public owner;

    function setUp() public {
        owner = address(this);
        myContract = new MyContract();
    }

    function testSetValue() public {
        myContract.setValue(42);
        assertEq(myContract.value(), 42);
    }

    function testFuzzValue(uint256 x) public {
        myContract.setValue(x);
        assertEq(myContract.value(), x);
    }
}

Deployment Script (Hardhat)

const hre = require("hardhat");

async function main() {
  const Contract = await hre.ethers.getContractFactory("MyContract");
  const contract = await Contract.deploy(100); // constructor args

  await contract.deployed();

  console.log("Contract deployed to:", contract.address);

  // Verify on Etherscan
  await hre.run("verify:verify", {
    address: contract.address,
    constructorArguments: [100],
  });
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Common Pitfalls

Unprotected Functions

// VULNERABLE
contract Vulnerable {
    address public owner;

    function changeOwner(address newOwner) public {
        owner = newOwner; // Anyone can call!
    }
}

// SAFE
contract Safe {
    address public owner;

    modifier onlyOwner() {
        require(msg.sender == owner);
        _;
    }

    function changeOwner(address newOwner) public onlyOwner {
        owner = newOwner;
    }
}

Delegatecall Danger

// DANGEROUS: delegatecall executes in caller's context
contract Proxy {
    address public implementation;

    function upgrade(address newImpl) public {
        // Ensure proper access control!
        implementation = newImpl;
    }

    fallback() external payable {
        address impl = implementation;
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
}

Timestamp Dependence

// WEAK: Miners can manipulate timestamp slightly
contract Vulnerable {
    function isLotteryTime() public view returns (bool) {
        return block.timestamp % 10 == 0; // Manipulable
    }
}

// BETTER: Use block.number or oracle
contract Better {
    function isLotteryBlock() public view returns (bool) {
        return block.number % 100 == 0;
    }
}

Useful Global Variables

Block and Transaction

Variable Description
block.number Current block number
block.timestamp Current block timestamp
block.difficulty Current block difficulty
block.gaslimit Current block gas limit
block.coinbase Current block miner address
msg.sender Sender of the message
msg.value Ether sent with call (wei)
msg.data Complete calldata
msg.sig Function signature (first 4 bytes)
tx.origin Original sender (avoid!)
tx.gasprice Gas price of transaction

Address Members

address payable recipient = payable(msg.sender);

// Balance
uint256 balance = recipient.balance;

// Transfer (2300 gas, throws on failure)
recipient.transfer(1 ether);

// Send (2300 gas, returns bool)
bool success = recipient.send(1 ether);

// Call (forwards all gas, returns bool)
(bool success, bytes memory data) = recipient.call{value: 1 ether}("");

// Code checks
uint256 size;
assembly {
    size := extcodesize(recipient)
}
bool isContract = size > 0;

Cryptographic Functions

// Hash functions
bytes32 hash1 = keccak256(abi.encodePacked("hello"));
bytes32 hash2 = sha256(abi.encodePacked("world"));

// Signature recovery
bytes32 messageHash = keccak256(abi.encodePacked(message));
bytes32 ethSignedHash = keccak256(
    abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash)
);
address signer = ecrecover(ethSignedHash, v, r, s);

Best Practices

Code Quality

  • Use latest Solidity version for security fixes
  • Follow checks-effects-interactions pattern
  • Implement reentrancy guards for external calls
  • Use OpenZeppelin contracts for standards
  • Write comprehensive tests (unit, integration, fuzzing)
  • Get professional audits before mainnet
  • Use events for off-chain indexing
  • Document with NatSpec comments

Security Checklist

// 1. Access control on sensitive functions
modifier onlyOwner() {
    require(msg.sender == owner);
    _;
}

// 2. Reentrancy protection
modifier nonReentrant() {
    require(!locked);
    locked = true;
    _;
    locked = false;
}

// 3. Input validation
function transfer(address to, uint256 amount) public {
    require(to != address(0), "Invalid address");
    require(amount > 0, "Invalid amount");
    require(balances[msg.sender] >= amount, "Insufficient balance");
    // ...
}

// 4. Integer overflow protection (Solidity >= 0.8.0 automatic)
// 5. Use pull over push for payments
// 6. Avoid tx.origin for authentication
// 7. Proper event emission
// 8. Emergency stop mechanism

NatSpec Documentation

/// @title A simple token contract
/// @author Your Name
/// @notice This contract implements a basic token
/// @dev All functions are tested
contract MyToken {
    /// @notice Transfer tokens to another address
    /// @dev Implements checks-effects-interactions pattern
    /// @param to The recipient address
    /// @param amount The amount to transfer
    /// @return success Whether the transfer succeeded
    function transfer(address to, uint256 amount) public returns (bool success) {
        // ...
    }
}

Also see