A Security-First Introduction to Solidity
Solidity Basics & Security Fundamentals
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 versioncontract: Similar to a classconstructor: Runs once at deploymentpublic: 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
onlyOwnerprotection
Post-Mortem:
- https://www.parity.io/blog/security-alert-2/
- https://consensys.io/diligence/blog/2017/11/parity-wallet-hack-again/
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 eithertrueorfalse. 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 & externallyexternal– cheaper for external callsinternal– only within contract / inheritanceprivate– 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:
- https://rekt.news/bzx-rekt/
- https://medium.com/@peckshield/peckshield-alert-bzx-attack-2-0-431f0a7c6c38
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 addressmsg.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 validationrevert→ manual rollbackassert→ 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:
- https://consensys.io/diligence/blog/2019/09/stop-using-soliditys-transfer-now/
- https://blog.openzeppelin.com/reentrancy-after-istanbul/
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