Solidity Programming FundamentalsIntroductionSolidity is a statically typed, contract-oriented programming language designed primarily for Ethereum-compatible blockchains. A Solidity program defines executable rules whose results are recorded in blockchain state. Unlike conventional software, a deployed contract cannot normally be edited in place, execution consumes gas, and interactions may be initiated by unknown or adversarial users. These characteristics make clarity, type safety, access control, and careful data management essential. This chapter develops the language foundations required to write practical Solidity contracts. It explains source structure, value and reference types, variables, mappings, arrays, functions, constructors, modifiers, events, custom errors, inheritance, interfaces, libraries, and the differences among storage, memory, and calldata. Learning ObjectivesBy the end of this chapter, a learner should be able to: - explain the structure of a Solidity source file;
- select suitable value and reference data types;
- distinguish state, local, immutable, and constant variables;
- implement mappings, arrays, functions, constructors, and modifiers;
- use events and custom errors appropriately;
- apply inheritance, interfaces, and libraries;
- choose correctly among storage, memory, and calldata;
- identify common design, security, gas, and governance concerns.
1. Solidity Source StructureA source file commonly begins with a compiler-version directive, followed by imports, declarations, interfaces, libraries, and contracts. // SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Counter {
uint256 private count;
function increment() external {
count += 1;
}
function getCount() external view returns (uint256) {
return count;
}
}The SPDX identifier communicates licensing information. The pragma directive restricts compilation to compatible compiler versions; it does not upgrade a deployed contract. Solidity 0.8 and later generally include checked arithmetic, meaning addition, subtraction, multiplication, and exponentiation revert on overflow or underflow unless an unchecked block is used. A contract contains state variables, functions, modifiers, events, errors, constructors, type declarations, and inherited members. Source organization should make the public interface and security boundaries easy to inspect. 2. Data TypesSolidity types are divided broadly into value types and reference types. 2.1 Value typesValue types are copied when assigned or passed to functions. Important examples include: | Type | Purpose |
|---|
bool | true or false values | uint8–uint256 | Unsigned integers | int8–int256 | Signed integers | address | 20-byte account or contract address | address payable | Address that can receive Ether through transfer or send | bytes1–bytes32 | Fixed-size byte sequences | enum | User-defined set of named options | function | Function references |
Integer widths are multiples of eight bits. For example, uint256 can represent values from 0 through 2²⁵⁶ − 1. Smaller integers may reduce storage when packed with compatible variables, but they do not necessarily reduce arithmetic or memory costs. The address type should be used for accounts and contracts. A contract should not assume that an address represents a human user: externally owned accounts and contracts can both call functions. 2.2 Reference typesReference types may refer to data rather than copy it immediately. They include arrays, structs, mappings, and strings. Their data location must usually be specified when used in functions or local variables. struct Product {
uint256 id;
string name;
address seller;
bool active;
}string is a dynamically sized UTF-8 byte sequence. For arbitrary binary data, bytes is generally more appropriate. Fixed-size bytes32 is often cheaper and easier to compare, but it cannot hold arbitrary-length content.
3. Variables and Data Locations3.1 State variablesState variables are stored permanently in contract storage and therefore affect the blockchain state and gas cost. uint256 public totalSupply;
address private administrator; A public state variable automatically generates a getter. private prevents direct access from Solidity contracts, but it does not make the value secret: blockchain storage can generally be inspected externally. constant variables are compile-time constants and cannot be changed. immutable variables are assigned once, normally in the constructor, and cannot be changed after deployment.
uint256 public constant FEE_BPS = 250;
address public immutable treasury; If a fee is expressed in basis points, the percentage is calculated as: percentage = basis points ÷ 100 Thus, 250 basis points equals 2.5 percent. The calculation assumes that the denominator used by the application is 10,000 basis points per 100 percent. 3.2 Storage, memory, and calldataSolidity provides three important data locations: | Location | Lifetime and use | Typical implication |
|---|
storage | Persistent contract state | Expensive to modify; survives transactions | memory | Temporary during one call | Mutable; disappears after execution | calldata | Read-only transaction input | Often efficient for external functions |
A storage reference can modify persistent state, so it must be used deliberately. uint256[] private values;
function updateFirst(uint256 newValue) external {
uint256[] storage localValues = values;
localValues[0] = newValue;
}By contrast, a memory copy does not alter the state array: function copyValues() external view returns (uint256[] memory) {
uint256[] memory temporary = values;
return temporary;
}For external functions receiving arrays or strings, calldata is normally appropriate when the function only reads the arguments. function sum(uint256[] calldata numbers)
external
pure
returns (uint256 total)
{
for (uint256 i = 0; i < numbers.length; i++) {
total += numbers[i];
}
}A common error is assuming that a storage reference is a harmless alias. It is not: changing it changes the contract’s persistent state. 4. Mappings, Arrays, and Structs4.1 MappingsA mapping associates keys with values: mapping(address => uint256) public balances;
mapping(address => mapping(address => bool)) public approved; Mappings do not store a list of keys and cannot be enumerated directly. Every possible key has a default value, such as zero for uint256 and false for bool. Consequently, a mapping alone cannot distinguish between “never inserted” and “inserted with the default value.” An additional boolean or an indexable key array is needed when that distinction matters. Mappings are suitable for direct lookup, such as balances, permissions, or ownership records. They are not suitable when the application must iterate over all entries. 4.2 ArraysArrays may be fixed-size or dynamic. uint256[10] public fixedScores;
uint256[] public scores;
function addScore(uint256 score) external {
scores.push(score);
}Dynamic arrays support push and pop. Deleting an element does not automatically preserve ordering. A gas-efficient removal technique is “swap and pop,” but it changes the order of elements: function removeAt(uint256 index) external {
require(index < scores.length, "out of bounds");
scores[index] = scores[scores.length - 1];
scores.pop();
}The function must document the ordering consequence. If ordering is semantically important, a different approach is required. 4.3 StructsStructs group related values: struct Proposal {
address proposer;
uint256 votes;
bool executed;
}
Proposal[] public proposals;Structs improve conceptual organization but may increase storage usage. Variables should be ordered thoughtfully because compatible small value types may be packed into one storage slot. Packing is an optimization, not a substitute for profiling and testing. 5. Functions and VisibilityA function declaration includes its name, parameters, visibility, mutability, and return values. function balanceOf(address account)
external
view
returns (uint256)
{
return balances[account];
}Visibility options are: external: callable from outside; external calldata is generally efficient;public: callable internally and externally;internal: callable only within the contract and derived contracts;private: callable only within the defining contract.
Mutability options are: view: must not modify state;pure: must not read or modify state;- payable functions: may receive Ether;
- no mutability keyword: may modify state but do not accept Ether by default.
Function visibility is not merely an organizational choice. An externally callable state-changing function is part of the contract’s attack surface and must be designed as a public protocol operation. Returning a value does not automatically make it private. Return data and event logs may be visible to users and indexing systems. 6. Constructors and ModifiersA constructor runs once during deployment. It is useful for establishing administrator addresses, initial parameters, and immutable variables. contract Vault {
address public immutable owner;
constructor(address initialOwner) {
require(initialOwner != address(0), "zero owner");
owner = initialOwner;
}
}A modifier defines reusable preconditions or postconditions: modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
function withdraw(uint256 amount) external onlyOwner {
// protected operation
}The underscore represents the function body. Modifiers should remain simple and predictable. A modifier that performs complex external calls can obscure control flow and create security risks. msg.sender is the immediate caller. tx.origin is the original transaction initiator and should not be used for authorization because intermediary contracts can exploit origin-based assumptions.
7. Events and ErrorsEvents create logs that off-chain applications can index and monitor. event Deposited(
address indexed account,
uint256 amount
);
emit Deposited(msg.sender, msg.value);Indexed parameters support efficient filtering, subject to the event indexing model. Events are not contract storage and cannot be used by Solidity code as authoritative state. Custom errors provide structured, gas-efficient failure information: error InsufficientBalance(uint256 requested, uint256 available);
if (amount > balances[msg.sender]) {
revert InsufficientBalance(amount, balances[msg.sender]);
}require remains useful for straightforward conditions and legacy interfaces. Custom errors are preferable when callers or debugging tools benefit from named failure data. Neither events nor errors should expose information that the application treats as confidential.
8. Inheritance, Interfaces, and Libraries8.1 InheritanceInheritance promotes reuse and supports abstract contracts. abstract contract Ownable {
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
}
contract Treasury is Ownable {
function setOwner(address newOwner) external onlyOwner {
owner = newOwner;
}
}Solidity uses a linearized method-resolution order for multiple inheritance. Developers must understand which overridden function is selected and must explicitly use override where required. Inheritance should express a genuine “is-a” relationship, not merely be used to obtain convenient code. 8.2 InterfacesAn interface declares externally callable functions without their implementation. interface IERC20Like {
function transfer(address to, uint256 amount)
external
returns (bool);
function balanceOf(address account)
external
view
returns (uint256);
}Interfaces support interoperability and abstraction. They do not guarantee that a target address behaves honestly or conforms perfectly to the declared semantics. External calls can fail, consume gas, return unexpected data, or trigger reentrancy. 8.3 LibrariesLibraries contain reusable functions. A library function may be attached to a type with using for. library MathTools {
function double(uint256 value)
internal
pure
returns (uint256)
{
return value * 2;
}
}
contract Example {
using MathTools for uint256;
function result(uint256 x) external pure returns (uint256) {
return x.double();
}
}Libraries are appropriate for stateless operations and shared validation. They should not conceal sensitive state changes or make security-critical behavior difficult to audit. 9. A Complete Applied ExampleThe following illustrative contract models a simple Ether vault. It demonstrates state, a constructor, a modifier, custom errors, events, mappings, and the checks-effects-interactions pattern. // SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract SimpleVault {
mapping(address => uint256) public deposited;
address public immutable administrator;
error ZeroAmount();
error InsufficientDeposit(uint256 requested, uint256 available);
error TransferFailed();
event Deposit(address indexed account, uint256 amount);
event Withdrawal(address indexed account, uint256 amount);
modifier nonZero(uint256 amount) {
if (amount == 0) revert ZeroAmount();
_;
}
constructor() {
administrator = msg.sender;
}
function deposit() external payable nonZero(msg.value) {
deposited[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
}
function withdraw(uint256 amount) external nonZero(amount) {
uint256 available = deposited[msg.sender];
if (amount > available) {
revert InsufficientDeposit(amount, available);
}
deposited[msg.sender] = available - amount;
(bool success, ) = payable(msg.sender).call{value: amount}("");
if (!success) revert TransferFailed();
emit Withdrawal(msg.sender, amount);
}
}The withdrawal function first checks the request, then updates internal accounting, and only then performs the external call. This ordering reduces the risk of reentrancy because a reentrant call observes the reduced balance. In production, developers should also consider a well-tested reentrancy guard, emergency controls, withdrawal policies, invariant testing, and the behavior of forced Ether transfers. Case study: an illustrative community grant contractBackground. A community organization wants members to vote on grants. Each proposal has a description, a recipient, a deadline, and a vote count. Problem. The organization needs transparent proposal records, protection against unauthorized execution, and an off-chain interface that can discover new proposals efficiently. Analysis. A struct is appropriate for proposal records, while a mapping can record whether an address has voted on a particular proposal. An array permits proposal identifiers to be enumerated. Events allow a web application to display new proposals without repeatedly scanning every state variable. Approach. The contract should validate nonzero recipients, enforce a voting deadline, prevent double voting, and separate voting from execution. A modifier such as onlyAdministrator may protect administrative operations. A custom error can report an invalid proposal identifier. The proposal array should not be allowed to grow without considering gas and user-interface consequences. Outcome. The design provides a transparent state model and an auditable event stream. It does not, by itself, prove that voters are unique humans, that the voting process is legally binding, or that the recipient will use funds appropriately. Learning points. Data structures should follow access patterns; events supplement rather than replace state; authorization and business rules must be explicit; and governance contracts require social as well as technical design. 10. Security, Governance, and Professional PracticeSolidity code is public infrastructure. Professional development should include unit tests, integration tests, fuzz testing, static analysis, compiler warnings, code review, and, for high-value systems, independent audit. Tests should cover both successful and reverting paths. Important risks include: - reentrancy through external calls;
- incorrect authorization or use of
tx.origin; - denial of service caused by unbounded loops;
- arithmetic, precision, and unit-conversion errors;
- unsafe assumptions about token standards;
- reliance on block timestamps or block variables for strong randomness;
- upgradeability risks and storage-layout incompatibility;
- loss or misuse of privileged keys;
- front-running and transaction-order dependence.
Gas efficiency matters because users pay for computation and state changes. However, optimizing before establishing correctness can introduce subtle bugs. A useful priority order is correctness, security, clear invariants, testability, and then measured optimization. Governance must define who can change parameters, pause activity, upgrade code, or recover funds. Privileged roles should be documented and, where appropriate, controlled by multisignature wallets, timelocks, or transparent community procedures. Legal and regulatory duties may apply to tokens, custody, voting, and financial activity; technical deployment does not remove those obligations. 11. Contemporary DevelopmentsModern Solidity practice increasingly uses custom errors, compiler checks, formal verification tools, fuzzing, standardized interfaces, and account-abstraction-compatible designs. Layer-2 networks make execution cheaper in many cases but do not eliminate the need for secure code. Contracts may also interact with oracles, cross-chain messaging systems, and off-chain automation, each of which introduces trust and failure assumptions. AI-assisted coding can help generate tests, documentation, and preliminary code review suggestions. It must not be treated as an auditor or authoritative source. Generated code requires compilation, adversarial testing, manual review, and verification against the intended specification. Best-Practice Checklist- Pin and test the compiler version used for deployment.
- State assumptions about callers, tokens, oracles, and privileged roles.
- Use explicit visibility and mutability declarations.
- Prefer custom errors for structured failures.
- Emit events for important state transitions.
- Avoid unbounded loops over user-controlled collections.
- Follow checks-effects-interactions and assess reentrancy.
- Use
calldata for read-only external reference parameters where suitable. - Distinguish storage references from memory copies.
- Test edge cases, failed calls, zero values, and boundary conditions.
Chapter SummarySolidity contracts combine persistent state with publicly callable functions. Value types are copied, whereas reference types require careful data-location choices. State variables use storage; temporary values use memory; and external input is commonly read from calldata. Mappings provide direct lookup but no enumeration, while arrays and structs support ordered and grouped data. Constructors establish deployment-time state, modifiers reuse preconditions, events expose state transitions to off-chain systems, and custom errors provide structured failure information. Inheritance, interfaces, and libraries support reuse and interoperability but must be used with a clear understanding of control flow and external-call risks. Secure Solidity development depends on explicit authorization, disciplined interaction ordering, testing, auditability, and governance design. Key TermsSolidity: A statically typed language for blockchain smart contracts. Contract: A deployed program containing code and persistent state. State variable: A variable stored persistently on the blockchain. Value type: A type whose assignment generally copies its value. Reference type: A type that refers to data such as an array or struct. Mapping: A key-to-value lookup structure without direct enumeration. Array: An ordered collection that may be fixed-size or dynamic. Struct: A user-defined group of named fields. Storage: Persistent contract data location. Memory: Temporary, mutable data location during execution. Calldata: Read-only input data supplied to an external call. Modifier: Reusable function precondition or postcondition logic. Constructor: Code executed once during deployment. Event: A blockchain log intended for off-chain consumption. Custom error: A named, structured revert condition. Inheritance: Reuse and extension of contract definitions. Interface: A declaration of externally callable functions. Library: Reusable code intended primarily for shared operations. Reentrancy: A repeated entry into a contract before an earlier call completes. Gas: The execution resource charged for blockchain operations. Review QuestionsShort Answer Questions- What is the difference between a state variable and a local variable?
- Why can a mapping not be directly enumerated?
- Distinguish
view from pure functions. - What purpose does an event serve?
- Why is
tx.origin unsuitable for authorization?
Long Answer Questions- Explain storage, memory, and calldata, including an example of when each is appropriate.
- Compare mappings, arrays, and structs for implementing an account registry.
- Explain constructors and modifiers and discuss their security implications.
- Describe how interfaces and libraries support modular contract development.
- Explain the checks-effects-interactions pattern and its relationship to reentrancy.
Analytical / Application Questions- Design the data structures and functions for a contract that records one vote per address per proposal. Justify each choice.
- A contract removes an array element by replacing it with the final element. Analyze the gas benefit and the effect on ordering.
- Review a withdrawal function that sends Ether before reducing the caller’s balance. Identify the vulnerability and propose a corrected sequence.
Multiple Choice Questions with AnswersQuestion 1Which data location provides persistent contract state? A. memory B. calldata C. storage D. stack Correct Answer: C. storage Question 2What is a principal limitation of a Solidity mapping? A. It cannot store address keys B. It cannot be directly enumerated C. It cannot store zero values D. It can only contain Boolean values Correct Answer: B. It cannot be directly enumerated Question 3Which function mutability allows reading state but prohibits state modification? A. pure B. view C. payable D. internal Correct Answer: B. view Question 4When does a constructor normally execute? A. Every time a function is called B. Once during contract deployment C. Only after a contract is destroyed D. Whenever an event is emitted Correct Answer: B. Once during contract deployment Question 5What is the main purpose of an event? A. To create persistent storage variables B. To provide indexed logs for off-chain consumers C. To prevent all external calls D. To replace function return values Correct Answer: B. To provide indexed logs for off-chain consumers Question 6Which statement about custom errors is correct? A. They are structured revert conditions B. They automatically authorize callers C. They can only be used in interfaces D. They permanently store error data Correct Answer: A. They are structured revert conditions Question 7Why is reducing a user balance before an external Ether call safer? A. It guarantees the call will succeed B. It follows checks-effects-interactions and limits reentrancy impact C. It removes all gas costs D. It makes the function pure Correct Answer: B. It follows checks-effects-interactions and limits reentrancy impact Question 8Which type is generally appropriate for read-only dynamic input to an external function? A. storage B. calldata C. immutable D. constant Correct Answer: B. calldata Question 9What does an interface primarily provide? A. A declaration of callable functions without implementation B. Automatic encryption of state C. A replacement for the compiler D. A dynamic array of contracts Correct Answer: A. A declaration of callable functions without implementation Question 10What is a key consequence of using swap-and-pop to remove an array element? A. The array becomes fixed-size B. The removed element is always preserved C. Element ordering may change D. The operation becomes read-only Correct Answer: C. Element ordering may change
|