Solidity Language FundamentalsIntroduction and ContextSolidity is a statically typed, contract-oriented programming language used primarily to develop smart contracts for Ethereum-compatible blockchain networks. A Solidity contract resembles a class in an object-oriented language, but its execution environment is fundamentally different: contract code executes on a blockchain, state changes are recorded permanently, and computational resources are limited by gas. A Solidity developer must therefore understand both programming fundamentals and blockchain constraints. Correct syntax is necessary, but it is not sufficient. Developers must also reason about visibility, transaction boundaries, persistent state, access control, data locations, gas consumption, and the consequences of irreversible execution. This chapter introduces the language features required to develop small but structurally sound contracts. Examples use Solidity version 0.8.x syntax. Compiler versions should always be checked against the deployed environment because language behavior and security features may change between releases. Learning ObjectivesBy the end of this chapter, you should be able to: - explain Solidity pragmas and source-file structure;
- distinguish value types from reference types;
- declare and use variables, constants, mappings, arrays, structs, and enums;
- apply operators and control-flow statements;
- define constructors and functions;
- select appropriate visibility and mutability modifiers;
- distinguish the
storage, memory, and calldata data locations; - identify common design, gas, and security considerations in Solidity contracts.
1. Solidity Source StructureA Solidity source file commonly contains a license identifier, a compiler pragma, imports, interfaces, libraries, and one or more contracts. // SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Example {
uint256 private value;
constructor(uint256 initialValue) {
value = initialValue;
}
function getValue() external view returns (uint256) {
return value;
}
}The SPDX comment identifies the software license. It is not executed by the EVM, but it is recommended for transparent source publication. The pragma statement gives instructions to the compiler. It does not change the compiler installed on a network and does not automatically upgrade a contract after deployment. The expression ^0.8.20 permits compiler versions from 0.8.20 up to, but not including, 0.9.0 under semantic-versioning conventions. Production projects should normally pin a compiler version or use a carefully tested compatible range. A contract may contain: - state variables;
- events;
- errors;
- modifiers;
- constructors;
- receive and fallback functions;
- ordinary functions;
- structs, enums, and user-defined types.
The order is partly a matter of style, but consistent organization improves review and maintenance. 2. Variables and TypesA variable is a named location or reference used to hold a value. Solidity is statically typed: a variable’s type is known at compile time and generally cannot be changed during execution. 2.1 State, local, and global variablesState variables are declared at contract level and persist in blockchain storage. They contribute to the contract’s long-term state and are comparatively expensive to write. Local variables are declared inside functions. Depending on their type and data location, they may exist temporarily in memory, calldata, or the EVM stack. Solidity also provides global blockchain-related variables and functions, including: block.timestamp: the timestamp associated with the current block;block.number: the current block number;msg.sender: the immediate caller;msg.value: the amount of native cryptocurrency sent with a payable call;address(this): the contract’s address.
These values must be used carefully. For example, block.timestamp is not a precision clock and should not be used as the sole source of randomness or for highly sensitive timing decisions. 2.2 Value typesValue types are copied when assigned or passed to a function. Common value types include: | Type | Purpose |
|---|
bool | true or false values | uint256 | Unsigned integer from 0 to 2²⁵⁶ − 1 | int256 | Signed integer | address | 20-byte blockchain address | address payable | Address capable of receiving native currency through Solidity transfer operations | bytes1 to bytes32 | Fixed-size byte sequences | enum | User-defined finite set of named options | | Contract types | References to deployed contract addresses with known interfaces |
Unsigned integers cannot represent negative values. Solidity 0.8.x automatically checks arithmetic operations for overflow and underflow and reverts when they occur, except inside an explicitly marked unchecked block. Integer widths range from 8 to 256 bits in increments of 8. For general-purpose values, uint256 is frequently appropriate because the EVM operates naturally with 256-bit words. Smaller types can be useful in packed storage structures, but packing should be measured rather than assumed to reduce gas in every situation. 2.3 Reference typesReference types do not necessarily copy all data when assigned. They refer to complex data such as arrays, structs, mappings, and strings. Their behavior depends strongly on the data location: storage, memory, or calldata. A reference value assigned from storage to a storage reference can refer to the same persistent data. A value copied into memory becomes an independent temporary copy. Confusing these behaviors can cause unintended state changes or unnecessary gas consumption. 3. Data LocationsEvery reference-type variable generally requires an explicit data location in function parameters, return values, and local declarations. | Location | Meaning | Typical use |
|---|
storage | Persistent contract state | State variables and deliberate in-place updates | memory | Temporary, mutable data during a call | Temporary arrays and constructed return data | calldata | Temporary read-only function input | External function parameters |
storage survives after a transaction completes. Writing storage is expensive because it changes blockchain state. memory exists only while a call executes and can be modified. calldata is supplied with the external call, is read-only, and often avoids copying input into memory.
For example: function sum(uint256[] calldata numbers)
external
pure
returns (uint256 total)
{
for (uint256 i = 0; i < numbers.length; i++) {
total += numbers[i];
}
}The array is read directly from calldata. An external function cannot use calldata for a state variable because state variables must persist in storage. 4. Arrays, Mappings, Structs, and Enums4.1 ArraysAn array stores multiple values of the same type. Fixed-size arrays specify their length, while dynamic arrays can grow or shrink. uint256[3] public fixedValues;
uint256[] private values; Dynamic arrays support operations such as push() and pop(). Removing an element by shifting all later elements can be expensive. If order is not important, a common constant-time pattern is to replace the removed element with the final element and then call pop(). function removeAt(uint256 index) external {
require(index < values.length, "invalid index");
values[index] = values[values.length - 1];
values.pop();
}This changes ordering and should be used only when that change is acceptable. 4.2 MappingsA mapping associates keys with values: mapping(address => uint256) public balances; A mapping does not store a list of keys and does not provide a length property or built-in iteration. Every possible key appears to have a default value, such as zero for uint256 or false for bool. Consequently, a mapping alone cannot distinguish between “no record exists” and “a record exists with the default value.” A separate existence flag or key index may be required. Mappings are especially useful for balances, permissions, ownership records, and vote counts. Mapping keys may be value types, but mappings cannot directly use dynamically sized arrays, mappings, or structs containing such types as keys. 4.3 StructsA struct groups related fields into a custom type: struct Student {
string name;
uint256 enrollmentYear;
bool active;
}
mapping(address => Student) private students;Structs improve conceptual clarity and allow a contract to model domain entities. Developers should consider whether storing strings on-chain is necessary, because persistent text can be expensive. A hash, identifier, or off-chain content reference may be more appropriate. 4.4 EnumsAn enum represents a finite set of named states: enum Status { Pending, Approved, Rejected }
Status public status;The first member has the default value, so status initially equals Pending unless explicitly changed. Enums are useful for workflows, but transitions should be validated so that invalid state changes cannot occur. 5. Operators and ExpressionsSolidity provides arithmetic, comparison, logical, bitwise, assignment, and conditional operators. Arithmetic operators include +, -, *, /, and %. Integer division truncates toward zero. Therefore, 5 / 2 produces 2 when both operands are integers. Division by zero reverts. Comparison operators include ==, !=, <, >, <=, and >=. Logical operators are &&, ||, and !. Short-circuit evaluation applies: in A && B, B is evaluated only if A is true; in A || B, B is evaluated only if A is false. The conditional operator has the form: condition ? valueIfTrue : valueIfFalse Explicit type conversions should be used carefully. Converting between signed and unsigned integers or narrowing a value to a smaller type can change its interpretation or truncate information. A safe design validates ranges before conversion. 6. Control FlowSolidity supports if, else, for, while, do ... while, and return. It also supports break and continue within loops. function classify(uint256 score) public pure returns (string memory) {
if (score >= 70) {
return "pass";
} else {
return "fail";
}
}Loops must be designed with gas limits in mind. A transaction that attempts too much work may exceed its gas limit and revert. Because mappings cannot be enumerated automatically, contracts commonly maintain a separate array of keys when iteration is essential. Such arrays can grow indefinitely, creating a long-term scalability problem. A loop whose bounds depend on user-controlled data should be reviewed carefully. Alternatives include pagination, off-chain computation with on-chain verification, or designing operations that process a fixed-size batch. 7. Functions, Constructors, Visibility, and Mutability7.1 Function structureA function may include a name, parameters, visibility, mutability, modifiers, and return values: function transfer(address recipient, uint256 amount)
external
returns (bool)
{
// implementation
return true;
}7.2 VisibilitySolidity provides four visibility levels: | Visibility | Callable externally? | Callable internally? | Typical meaning |
|---|
public | Yes | Yes | Externally callable and internally accessible | external | Yes | Through an external call | Public interface, often efficient for calldata inputs | internal | No direct external call | Yes, by contract and derived contracts | Internal implementation | private | No | Only within declaring contract | Restricted implementation detail |
private does not make blockchain data secret. State remains observable through blockchain data and node tools. Visibility controls Solidity-level access, not confidentiality.
7.3 State mutabilityThe principal mutability modifiers are: view: may read state but must not modify it;pure: may not read or modify contract state;payable: may receive native cryptocurrency;- no modifier: may modify state, subject to execution rules.
A view function called locally through an RPC provider normally does not require a user transaction, but a transaction can still invoke it as part of a state-changing operation. pure functions are appropriate for deterministic calculations based only on arguments and local variables. 7.4 ConstructorsA constructor runs once, during deployment. It initializes state and can receive deployment parameters. contract Treasury {
address public immutable administrator;
constructor(address initialAdministrator) {
require(initialAdministrator != address(0), "zero address");
administrator = initialAdministrator;
}
}Constructors are not callable after deployment. Initialization logic must therefore be complete and carefully tested. In upgradeable-contract systems, constructors do not initialize proxy storage in the usual way; an explicit initializer pattern is used instead. 7.5 require, revert, and custom errorsInput and authorization conditions can be enforced with require: require(msg.sender == administrator, "not authorized"); revert is useful for explicit branches. Custom errors are generally more gas-efficient than long revert strings:
error NotAuthorized(address caller);
if (msg.sender != administrator) {
revert NotAuthorized(msg.sender);
}A revert undoes state changes made during the current call frame and returns an error to the caller. 8. A Complete Applied ContractThe following illustrative contract models a simple enrollment registry. It demonstrates a pragma, state variables, a struct, enum, mapping, array, constructor, access control, data locations, events, and functions. // SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract EnrollmentRegistry {
address public immutable administrator;
enum Status { Pending, Active, Completed, Cancelled }
struct Enrollment {
string courseCode;
uint256 year;
Status status;
}
mapping(address => Enrollment) private enrollments;
address[] private enrolledStudents;
event EnrollmentCreated(address indexed student, string courseCode, uint256 year);
event StatusChanged(address indexed student, Status status);
error NotAdministrator();
error AlreadyEnrolled();
error NotEnrolled();
constructor() {
administrator = msg.sender;
}
modifier onlyAdministrator() {
if (msg.sender != administrator) revert NotAdministrator();
_;
}
function enroll(
address student,
string calldata courseCode,
uint256 year
) external onlyAdministrator {
if (enrollments[student].year != 0) revert AlreadyEnrolled();
enrollments[student] = Enrollment({
courseCode: courseCode,
year: year,
status: Status.Pending
});
enrolledStudents.push(student);
emit EnrollmentCreated(student, courseCode, year);
}
function activate(address student) external onlyAdministrator {
if (enrollments[student].year == 0) revert NotEnrolled();
enrollments[student].status = Status.Active;
emit StatusChanged(student, Status.Active);
}
function getEnrollment(address student)
external
view
returns (Enrollment memory)
{
return enrollments[student];
}
function studentCount() external view returns (uint256) {
return enrolledStudents.length;
}
}Case analysis: Enrollment RegistryBackground. A training institution wants a tamper-resistant record of enrollment status that authorized administrators can update and students can inspect. Problem. The institution needs structured records, controlled updates, and transparent read access without allowing arbitrary users to alter records. Approach. The contract assigns the deployer as administrator, stores each enrollment in a mapping, keeps an array for counting enrolled students, and represents lifecycle state with an enum. The onlyAdministrator modifier centralizes authorization. Events create an indexed history useful to off-chain applications. Outcome. A front-end can call getEnrollment and studentCount through an RPC provider, while only the administrator can create or activate records. The contract demonstrates that blockchain transparency does not imply unrestricted write access. Limitations. The contract does not verify that a person controls the student address, does not support correction of a mistaken enrollment, and stores course text directly on-chain. A production design would define identity, correction, privacy, retention, and governance requirements before deployment. Learning points. A mapping offers efficient lookup but no enumeration; the additional array solves counting but introduces duplication and consistency obligations. Events support external indexing but are not a replacement for state. Access control must be explicit, and blockchain records should not contain unnecessary personal information. 9. Professional Practice, Security, and GovernanceSolidity programs are public, financially consequential, and difficult to change after deployment. Professional development should therefore include compiler pinning, automated tests, static analysis, code review, and independent security review for high-value contracts. Important risks include: - authorization failures caused by missing or incorrect access checks;
- reentrancy when an external call is made before internal state is finalized;
- denial of service from unbounded loops or unexpectedly expensive operations;
- incorrect assumptions about timestamps, caller identity, or transaction ordering;
- arithmetic and type-conversion errors;
- accidental exposure of confidential data;
- upgradeability risks, including unauthorized upgrades or storage-layout errors.
The checks-effects-interactions pattern is a useful baseline: validate conditions, update internal state, and only then interact with external contracts. It is not a complete security solution, but it reduces several common risks. Events should record important state transitions, while sensitive personal data should generally be kept off-chain or represented by a carefully managed commitment. Even if a user interface hides information, data published to a public blockchain may remain accessible indefinitely. Gas is a resource constraint rather than merely a financial concern. Storage writes, dynamic data, loops, and external calls should be evaluated during design. Optimization must not weaken clarity or security; an inexpensive vulnerable contract is still a failed system. 10. Contemporary Development PracticesModern Solidity development commonly uses unit tests, property-based testing, fuzzing, static analyzers, formal specifications for critical invariants, and continuous integration. Development frameworks can compile contracts, run local blockchain nodes, simulate transactions, and produce deployment artifacts. AI-assisted coding can help generate test cases, explain compiler errors, and identify possible edge cases. However, generated code must never be trusted without compilation, testing, manual review, and security analysis. AI systems may reproduce obsolete syntax, overlook authorization flaws, or propose unsafe assumptions about blockchain behavior. A robust workflow is: - define state invariants and trust boundaries;
- write the smallest clear contract interface;
- compile with a fixed, reviewed compiler version;
- test normal, boundary, and adversarial cases;
- inspect gas behavior and failure modes;
- perform security review;
- deploy first to a test network;
- verify source code and monitor events after deployment.
Chapter SummarySolidity is a statically typed, contract-oriented language whose programs execute under blockchain and gas constraints. A source file commonly contains a license identifier, pragma, declarations, and one or more contracts. Variables may be state, local, or global-context values, and Solidity distinguishes value types from reference types. Mappings provide efficient key-based lookup but do not store keys or support automatic enumeration. Arrays support ordered collections but may create gas and scalability concerns. Structs model related data, while enums represent finite states. Operators and control-flow statements provide ordinary programming capabilities, but integer division, overflow checks, type conversions, and loop bounds require care. Functions are characterized by visibility and mutability. Constructors execute once at deployment. storage, memory, and calldata have different persistence, mutability, and cost implications. Secure professional practice requires explicit authorization, careful external interaction, testing, review, privacy awareness, and governance planning. Key TermsSolidity: A programming language for Ethereum-compatible smart contracts. Pragma: A compiler directive specifying permitted compiler versions or behaviors. Smart contract: Executable blockchain code and its persistent state. State variable: A contract-level variable stored persistently on-chain. Value type: A type whose assignment or parameter passing normally copies its value. Reference type: A type that refers to complex data such as arrays or structs. Mapping: A key-value data structure with implicit default values. Array: An ordered collection of values of one type. Struct: A user-defined type containing named fields. Enum: A user-defined type containing a finite set of named values. Storage: Persistent blockchain data location. Memory: Temporary mutable data location during execution. Calldata: Temporary read-only external function input. Constructor: A function executed once during deployment. Visibility: The rule determining where a function or variable can be accessed. Mutability: A function property indicating whether it reads, changes, or receives value. Modifier: Reusable function precondition or execution logic. Event: A log emitted for off-chain monitoring and indexing. Gas: The computational resource consumed by EVM execution. Reentrancy: An unwanted nested call into a contract before an earlier operation is complete. Review QuestionsShort Answer Questions- What is the purpose of a Solidity pragma?
- How does a mapping differ from an array?
- Distinguish
storage, memory, and calldata. - What is the difference between
view and pure? - Why does a constructor require careful initialization logic?
Long Answer Questions- Explain Solidity value and reference types, including the role of data locations.
- Compare
public, external, internal, and private visibility. - Describe the uses and limitations of arrays, mappings, structs, and enums.
- Explain how gas limitations influence loops and data structures.
- Discuss security and privacy considerations in deploying a Solidity contract.
Analytical / Application Questions- Design a data model for a blockchain-based library system using mappings, arrays, structs, and enums. Explain the trade-offs.
- A contract stores a mapping of users to balances and must list all users. Propose a design and analyze its scalability limitations.
- Review the Enrollment Registry example and propose changes needed to support multiple administrators, student-controlled data correction, and privacy-preserving course references.
Multiple Choice Questions with AnswersQuestion 1Which data location is persistent between transactions? A. memory B. calldata C. storage D. stack Correct Answer: C. storage Question 2What is a principal limitation of a Solidity mapping? A. It can store only Boolean values B. It cannot directly enumerate its keys C. It cannot use address keys D. It always stores values in memory Correct Answer: B. It cannot directly enumerate its keys Question 3Which function mutability modifier permits reading state but not modifying it? A. pure B. view C. payable D. external Correct Answer: B. view Question 4When does a contract constructor normally execute? A. Every time a function is called B. Once during deployment C. When an event is emitted D. After every block Correct Answer: B. Once during deployment Question 5Which visibility permits external calls and internal calls without an external message call? A. private B. internal C. public D. calldata Correct Answer: C. public Question 6What happens when integer division evaluates 5 / 2 with integer operands? A. It returns 2.5 B. It returns 3 C. It returns 2 D. It reverts automatically Correct Answer: C. It returns 2 Question 7Why might a contract maintain both a mapping and an array of addresses? A. To make the mapping private automatically B. To support lookup and separately track keys for iteration or counting C. To prevent all state changes D. To convert addresses into strings Correct Answer: B. To support lookup and separately track keys for iteration or counting Question 8Which data location is read-only and commonly used for external function inputs? A. storage B. memory C. calldata D. immutable Correct Answer: C. calldata Question 9What is the default first member of an enum used for when no value is explicitly assigned? A. The maximum enum value B. The first declared member C. A random member D. An invalid value Correct Answer: B. The first declared member Question 10What is a major risk of an unbounded loop over a growing on-chain array? A. The contract automatically changes compiler versions B. The transaction may exceed its gas limit C. The array becomes a mapping D. The loop makes all functions pure Correct Answer: B. The transaction may exceed its gas limit
|