← Back to list

A Security-First Introduction to Solidity

Solidity Basics & Security Fundamentals

@AayJay · 2026-02-02 07:57 · 0 claps · 6.6 min read
#solidity #web3-security #solidity-tutorial #solidity-contract-audit #solidity-development
Open on Medium ↗
Wiki topics: CRY · Crypto & Web3

A Security-First Introduction to Solidity

Solidity Basics & Security Fundamentals

Solidity is the primary language used to write smart contracts on Ethereum and EVM-compatible blockchains. While learning Solidity syntax is easy, writing secure Solidity code is hard.

In this blog, we’ll cover:

  • Core Solidity fundamentals
  • Realistic code examples

If you’re aiming to build or audit smart contracts, this mindset will save you from expensive mistakes.

1. What Is a Smart Contract?

A smart contract is a program stored on the blockchain that:

  • Executes deterministically
  • Cannot be modified after deployment
  • Controls assets (ETH, tokens, NFTs)

Once deployed, bugs are forever unless mitigated via upgrades or governance.

That’s why auditors don’t just read code — they read intent vs behavior.

2. Basic Solidity Contract Structure

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20; // Sets compiler version for security and compatibility.
contract HelloWorld { // Starts the contract's logic and storage scope.
    string public message; // Variable stored on-chain that anyone can view.
    constructor(string memory _message) { // Sets the initial value during deployment.
        message = _message; // Saves the input to permanent storage.
    } 
    function updateMessage(string memory _newMessage) public { // Allows anyone to change the stored string.
        message = _newMessage; // Overwrites the old string with new data.
    } 
}

Key Concepts

  • pragma solidity: Compiler version
  • contract: Similar to a class
  • constructor: Runs once at deployment
  • public: Automatically generates a getter

Security Note: No access control: Anyone can call updateMessage().

Common finding: Missing access control on state-changing functions

Exploit: Parity Multisig Freeze (2017)

Loss: ~$300M ETH frozen forever Root Cause: Unprotected initialization function

An attacker called the library’s init function, took ownership, then self-destructed it.

Auditor’s Red Flag

  • Initialization logic outside constructor
  • No onlyOwner protection

Post-Mortem:

3. State Variables & Data Types

contract TypesExample { // Starts the contract and its state definitions.
    uint256 public count; // Stores a positive whole number up to 2^256-1.
    address public owner; // Stores a 20-byte Ethereum wallet or contract address.
    bool public paused; // Stores a simple true or false binary state.
}

Solidity provides several built-in data types that are used to store and manage state within smart contracts.

  • **uint256** An unsigned 256-bit integer. This is the most commonly used numeric type in Solidity and is the default choice for counters, balances, and amounts.
  • **address** Represents an Ethereum account or a smart contract address. It is commonly used for ownership, permissions, and value transfers.
  • **bool** A Boolean value that can be either true or false. Often used for flags such as pause states or access conditions.
  • **string** UTF-8 encoded text data. Typically used for metadata or user-facing information but avoided in gas-sensitive logic.

Security Note:

In Solidity, state variables are automatically initialized to default values. For example, a uint256 variable defaults to 0 if it is not explicitly set.

This behavior does not raise compiler errors, but it can introduce subtle logic bugs. Developers may unintentionally rely on a variable being initialized when, in reality, its default value allows certain checks or conditions to be bypassed.

Historical Exploit: BeautyChain (BEC) Overflow

Minted billions of tokens due to overflow.

Post-Mortem:

4. Functions & Visibility

contract VisibilityExample { // Defines a contract to demonstrate data access levels.
    uint256 private secret; // State variable readable on-chain but restricted to this contract's logic.
    function setSecret(uint256 _secret) external { // Function callable only from outside the contract (saves gas).
          secret = _secret; // Updates the private variable with the provided input.
      }
      function getSecret() public view returns (uint256) { // Function callable internally and externally to read data.
          return secret; // Returns the value of the private variable to the caller.
      }
}

Visibility Keywords

  • public – callable internally & externally
  • external – cheaper for external calls
  • internal – only within contract / inheritance
  • private – only within this contract

Security Note: private ≠ secret

  • All blockchain data is publicly readable.

Example: bZx Attacks (2020)

Attackers chained public functions in unintended ways.

Post-Mortem:

5. msg.sender & msg.value

contract SenderExample { // Defines a contract to track ownership and funds.
    address public owner; // Stores the address of the contract's controller.
    constructor() { // Executes only during the contract's creation.
            owner = msg.sender; // Assigns the deployer's address as the owner.
        }
    function deposit() public payable {} // Enables the contract to accept and store ETH.
}
  • msg.sender → caller address
  • msg.value → ETH sent with transaction

Security Note: Never trust msg.sender blindly in:

  • Meta-transactions
  • Cross-contract calls
  • Proxy setups

Real-World Exploit: Wormhole Bridge Hack (2022)

Loss: ~$325M Root Cause: Guardian signature verification bypass

The contract trusted a message that looked authorized — but verification was incomplete.

Lesson

  • msg.sender ≠ real authority
  • Cross-chain calls require explicit validation

Post-Mortem:

6. Require, Revert & Assert

function withdraw(uint256 amount) public { // Public function allowing users to pull ETH from the contract.
    require(amount > 0, "Invalid amount"); // Reverts the transaction if the requested amount is zero.
    require(amount <= address(this).balance, "Insufficient balance"); // Checks if the contract has enough ETH to pay out.
    payable(msg.sender).transfer(amount); // Sends the specified ETH to the caller; reverts on failure.
}

Differences

  • require → input & condition validation
  • revert → manual rollback
  • assert → invariants (should never fail)

Security Note: assert failure consumes all gas Use it only for internal invariants.

Real-World Pattern

Attackers intentionally trigger assert to:

  • Block protocol functionality
  • Cause denial of service

Post-Mortem:

7. Ether Transfers

Dangerous Pattern

function withdraw() public { // Function allowing anyone to trigger a full balance withdrawal.
    payable(msg.sender).transfer(address(this).balance); // Sends all contract ETH to the caller; reverts if it fails.
}

Safer Pattern

function withdraw(uint256 amount) public { // Allows users to withdraw their specific deposited balance.
    require(amount <= balances[msg.sender], "Not enough balance"); // Ensures the user has enough funds recorded in the contract.
    balances[msg.sender] -= amount; // Updates state first to prevent reentrancy (Checks-Effects-Interactions).
        (bool success, ) = msg.sender.call{value: amount}(""); // Low-level call to send ETH; returns a success boolean.
        require(success, "ETH transfer failed"); // Reverts the whole transaction if the ETH transfer was unsuccessful.
}

Security Note: If state is updated after external call → reentrancy attack.

Real-World Exploit: The DAO

The attacker re-entered withdraw() before balance update, draining funds repeatedly.

Post-Mortem:

8. Reentrancy Vulnerability (Classic)

function withdraw(uint256 amount) public { // Public function to withdraw a specific ETH amount.
    require(balances[msg.sender] >= amount); // Check: Verifies the caller has enough stored balance.
(bool success, ) = msg.sender.call{value: amount}(""); // Interaction: Sends ETH to caller (Vulnerable: occurs before state update).
    require(success); // Verification: Reverts the transaction if the ETH transfer fails.
    balances[msg.sender] -= amount; // Effect: Deducts amount from balance (Too late: allows reentrancy).
}

What’s Wrong?

External call happens before state update

Fix (Checks-Effects-Interactions)

balances[msg.sender] -= amount; // Effect: Updates the user's balance locally before sending any funds.
(bool success, ) = msg.sender.call{value: amount}(""); // Interaction: Sends ETH; prevents reentrancy since balance is already zeroed.
require(success); // Verification: Ensures the transaction only completes if the ETH was successfully sent.

Security Note: Any external call is attacker-controlled code execution

Real-World Exploit: Uniswap + ERC777 (2020)

Loss: ~$300K Root Cause: Token hooks re-entered protocol logic

Any external call = attacker-controlled code.

Post-Mortem:

9. Access Control (Most Common Bug Class)

contract Vault { // Defines a contract for restricted fund storage.
    address public owner; // State variable to store the authorized controller's address.
    constructor() { // Runs once to set the initial state during deployment.
            owner = msg.sender; // Sets the person who deployed the contract as the owner.
        }
        function withdrawAll() public { // Function to empty the contract's ETH balance.
            require(msg.sender == owner, "Not owner"); // Access Control: Reverts if the caller is not the owner.
            payable(owner).transfer(address(this).balance); // Sends the entire contract balance to the owner's address.
        }
}

Security Note: Always verify:

  • Who can call this?
  • Can ownership change?
  • Is there a missing modifier?

Real-World Example: Beanstalk Governance Attack (2022)

Loss: ~$182M Root Cause: Flash-loan governance takeover

Ownership and permissions were temporarily acquired.

  • Access control must consider economic attacks
  • Governance ≠ safety

Post-Mortem:

10. Modifiers

modifier onlyOwner() { // Defines a reusable piece of logic to restrict function access.
    require(msg.sender == owner, "Not owner"); // Validates that the caller is the authorized owner.
    _; // A special symbol that tells Solidity to execute the rest of the function code here.
}
function withdrawAll() public onlyOwner { // Applies the modifier to this function.
    payable(owner).transfer(address(this).balance); // Transfers all contract funds to the owner.
}

Security Note: Modifiers can:

  • Hide logic
  • Change execution order
  • Introduce reentrancy if poorly written

11. Integer Overflow & Solidity 0.8+

Before 0.8:

count += 1; // could overflow

After 0.8:

  • Automatic overflow checks
  • Reverts on overflow

Security Note: Still watch for:

  • unchecked {} blocks
  • Custom math libraries

12. Events & Logging

event Withdraw(address indexed user, uint256 amount); // Defines a log structure; 'indexed' allows off-chain tools to filter by address.
function withdraw(uint256 amount) public { // Defines the logic for a withdrawal transaction.
    emit Withdraw(msg.sender, amount); // Triggers the log, saving the caller's address and amount to the blockchain's transaction receipts.
}

Why Events Matter

  • Off-chain monitoring
  • Forensics
  • Incident response

Security Note: Missing events ≠ vulnerability, but they reduce observability

Post-Mortem:

https://blog.openzeppelin.com/monitoring-smart-contracts/

13. Common Audit Checklist for Beginners

When reviewing Solidity code, always ask:

  • Who can call this function?
  • Is there an external call?
  • Is state updated before or after?
  • Any unchecked math?
  • Can this be called twice?
  • Is initialization protected?

Final Thoughts

Solidity is simple on the surface but dangerous by default.

The best Solidity developers:

  • Write less code
  • Assume users are malicious
  • Think like attackers first

If you learn Solidity with auditing in mind, you’ll naturally write safer contracts — and that skill is rare and valuable.


메타데이터
post_id
d5bd2ae3c090
slug
a-security-first-introduction-to-solidity-d5bd2ae3c090
url
https://medium.com/@hackeroneop/a-security-first-introduction-to-solidity-d5bd2ae3c090
canonical_url
https://medium.com/@hackeroneop/a-security-first-introduction-to-solidity-d5bd2ae3c090
author_url
https://medium.com/@hackeroneop
status
ok
fetched_at
2026-08-08 08:35:13