General Fundamentals
Solidity Basics
Vocabulary
contract contract constructor constructor constant constant
Header
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24; // fixed version
pragma solidity ^0.8.24; // minimum version
pragma solidity >=0.8.19 < 0.9;
Data Types
Basic Variable Types (Value Types)
uint // 0
int // 0
strig // ''
bool // false
address // 0x0000...0000000000 40 zeros
bytes32 // 0x0000...00000000 64 zeros
You can check the maximum and minimum values of int
int public b = type(int).min;
int public c = type(int).max;
Shorthand
int defaults to int256
uint defaults to uint256
Reference Types
int[]
mapping(int->address)
bytes Types
https://chatgpt.com/s/t_68be42d2fed0819184d8b148f950e371 https://chatgpt.com/s/t_68be4467d6c48191864536636d0f6dbe
Types like bytes1, bytes2, bytes3, bytes4, ..., bytes32 — i.e. with a number appended — represent fixed-length byte arrays. The maximum is bytes32; there is no bytes64, a limitation imposed by the EVM. bytes represents a variable-length array that can be very long, theoretically up to a size of 2^256 - 1. Common types: bytes4: the type of a function selector bytes20: the address type bytes32: the result of keccak256, the size of a storage slot
pragma solidity ^0.8.24;
contract Example {
bytes4 public sel;
bytes32 public hash;
bytes public data;
constructor() {
// compute the function selector
sel = bytes4(keccak256("transfer(address,uint256)"));
// compute the hash
hash = keccak256(abi.encodePacked("hello"));
// dynamic byte array
data = abi.encodePacked(uint256(123), address(this));
}
}
unchecked
Version 0.8 introduced automatic overflow checking. For statements wrapped in unchecked, no error is raised when data overflows; instead it wraps around starting from 0. Normal statements would revert. But unchecked effectively tells the interpreter not to check for overflow, so it saves some gas.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Math{
uint8 public num;
constructor(uint8 init){
num = init;
}
function inc() external returns(uint8){
unchecked{
return num += 1;
}
}
function edd() external returns(uint8){
return num -=1;
}
function getMax() external pure returns(uint8){
return type(uint8).max;
}
}
Member Methods of bytes and string
https://docs.soliditylang.org/zh-cn/v0.8.24/types.html#bytes-concat-string-concat
bytes.concat(...) returns (bytes memory): concatenates a variable number of arguments into a single byte array.
string.concat(...) returns (string memory): concatenates a variable number of arguments into a single string.
Variables
State Variables
If you think of a contract as a class, state variables are the class's member variables. They are variables defined outside of any function but inside the contract. State variables are recorded on-chain. You can also think of them as the contract's global variables.
Local Variables
Defined inside a function within a contract. Their lifetime spans a single execution, from creation to destruction; they do not persist permanently on-chain.
Global Variables https://docs.soliditylang.org/zh-cn/v0.8.24/units-and-global-variables.html#index-3
Solidity's global variables are actually a set of built-in variables, such as msg.sender, block.xxxx, and so on.
Constants
A state variable defined with the constant keyword is called a constant, conventionally written in uppercase. Note that a constant is a state variable, meaning it is defined outside of any function but inside the contract. Because a constant is fixed and never changes, if a function only returns a constant, that function can be marked pure rather than view.
// As pure vs. view shows, if a function returns a constant it is effectively an internal operation, so it should be marked pure
contract constantVar{
uint public constant A=122;
function getVar() external pure returns (uint){
uint b = 3;
return b+A;
}
}
Control Flow Statements
if...else
contract processNumber{
function ifElse(uint x) external pure returns(uint){
if (x<10){ // wrap the condition in parentheses
return 1;
}else if (x < 20){ // it's "else if", not a shorthand
return 2;
}else{
return 3;
}
}
function tenary(uint x) external pure returns(uint){
return x<10 ? 1 : x<20 ? 2 :3; // nested ternary expression
} // you can replace the else branch with another statement, forming a nested ternary expression
}
for and while
If the compiler reports an error similar to the following, it likely means a list index went out of range
output 0x4e487b710000000000000000000000000000000000000000000000000000000000000032
contract Summation{
function sum(uint x) external pure returns(uint){
uint res = 0;
for (uint i=0; i< x; i++){ // standard C-style syntax
res += i;
}
return res;
}
function sumWhile(uint x) external pure returns(uint){
uint total = 0;
uint i = 0;
while(i < x){
total += i;
i +=1;
}
return total;
}
}
Errors
https://docs.soliditylang.org/zh-cn/v0.8.24/cheatsheet.html#index-6
assert(bool condition): if the condition is false, abort execution and revert state changes (used for internal errors).
require(bool condition): if the condition is false, abort execution and revert state changes (used for invalid input or errors in external components).
require(bool condition, string memory message): if the condition is false, abort execution and revert state changes (used for invalid input or errors in external components). Also provides an error message.
revert(): abort execution and revert state changes.
revert(string memory message): abort execution and revert state changes, providing an explanatory string.
// using a custom error is cheaper on gas than require + a string message
error NotOwner(string message);
modifier onlyOwner(){
if (msg.sender != i_Owner){
revert NotOwner("only owner can de this");
}
// require(msg.sender == i_Owner, "only owner can de this");
_;
}
Custom Errors: the error keyword
error MyError(address sender, uint value);
Throwing an Error
revert MyError(msg.sender, x);
Checking for Errors
require(x > 10, "x <= 10");
assert(x >10);
Reference Types
Array
Creation
Fixed-length array
uint256[3] public names = [1,2,3];
uint256[3] public names; // with no initial value set, all elements are zero
Dynamic array: the array length is variable and can be extended with push.
uint256[] public names; // initial length is 0; names[0], names[1] will revert. At this point names only supports growth, but has not grown yet
uint256[] public names = [1,2,3]; // fills 1,2,3 at the front
names = new uint256[](3); // this new form can only create dynamic arrays. The 3 sets the length to 3; these three elements all have an initial value of 0. When you later push, the index starts at 4
names = new uint256[3](3); // this form does not exist. With the new keyword the [] is left empty. Remember that the bracketed-number form creates a fixed-size array; new can only create dynamic arrays
Properties
Getting the length — note this is a property, not a method
uint[] a;
a.length
Adding
int[] a; // no fixed length specified — a dynamic array
int[3] b = [1,2,3] // fixed length
a.push(2) // fixed-length arrays do not support push, since the length is already fixed and push would increase the length
Removing
a.pop() pops the last element; does not return the value
To retrieve the last value and then delete it, write it like this
uint val = a[a.length-1]
a.pop()
delete a[1]; // this resets a[1] to that type's default value
Two deletion approaches First, preserve ordering, higher gas
function orderDelEle(uint index) external {
require(index < arr.length, "out of index");
for(uint i = index; i< arr.length-1; i++){
arr[i] = arr[i+1];
}
arr.pop();
}
Second, simply swap the target index with the last element
function delEle(uint index) external {
require(index < arr.length, "out of index");
arr[index] = arr[arr.length -1];
arr.pop();
}
Updating
delete a[1]; // resets that index to the default value
a[2] = 3; // the usual way to reassign
address[] public funders; // declare a variable-length empty array
// using new here treats address[] as an object or type
funders = new address[](funders.length); // assign the existing array a new zero-address array of the same length
Reading
uint val = a[index] // read by index
A function returning an array type
contract Sale{
uint256[] public vestingPortionsUnlockTime; // token distribution times
uint256[] public vestingPercentPerPortion; // token unlock/distribution percentages
function getVestingInfo() external view returns (uint256[] memory, uint256[] memory){ // get all distribution info, i.e. the staged unlock times and share information
return (vestingPortionsUnlockTime, vestingPercentPerPortion);
}
}
Mappings (Dictionaries)
mapping(address => uint) public balance; //
mapping(address => mapping(address => bool)) public isFriend; // nested
Add, remove, update, and read — just the usual use of [] to read or assign by key
Deletion:
balances[msg.sender] = amount;
delete balance[msg.sender]; // reading balance[2] again then returns the default value
balances[msg.sender]
Example
To implement an iterable mapping, you need an additional mapping and an array alongside it
contract SimpleBank{
mapping(address => uint) public balances; // store balances
mapping(address => bool) public isCustomer; // convenient membership check
address[] public customers; // effectively stores all the keys; iterating this array lets you traverse all values in balances
address public owner;
constructor(){
owner = msg.sender;
}
modifier onlyOwner(){
require(owner == msg.sender, "only owner can do this");
_;
}
function clearCustomer() external onlyOwner {
customers = new address[](0);
}
function deposit(uint amount) external returns(uint){
if (isCustomer[msg.sender] == false){
balances[msg.sender] = amount;
isCustomer[msg.sender] = true;
customers.push(msg.sender);
}else{
balances[msg.sender] += amount;
}
return balances[msg.sender];
}
function withdrawn(uint amount) external returns(uint){
require(balances[msg.sender] >= amount, "Insufficient Balance");
balances[msg.sender] -= amount;
return balances[msg.sender];
}
function checkBalance() external view returns(uint){
return balances[msg.sender];
}
function getSize() public view returns(uint){
return customers.length;
}
function getAllCustomers() external view returns(address[] memory){
uint size = getSize();
address[] memory _customers = new address[](size);
for (uint i = 0; i< size; i++){
_customers[i] = customers[i];
}
return _customers;
}
function delSomeone(address addr) external onlyOwner{
delete isCustomer[addr];
delete balances[addr];
}
function firstBalance() external view returns(uint, uint){
return (balances[customers[0]], balances[customers[customers.length-1]]);
}
}
Structs (struct)
The official definition is: a group of variables, representing grouped data. A struct cannot contain methods; it only holds basic types and reference/mapping types. You can think of it as a custom type, similar to uint, int, and so on.
Definition
struct Vehicle{
string make;
string model;
uint year;
address owner;
}
Once you have the Vehicle struct, you can define variables of type Vehicle, arrays of type Vehicle, and mappings of type Vehicle
Vehicle public car; // define a single car
Vehicle[] public cars; // define an array to hold Vehicle data
mappint(address => Vehicle[]); // define a mapping where each address has an array of Vehicle
function buy(string memory make, string memory model, uint year) external{
Vehicle memory car = Vehicle(make, model, year, msg.sender); // first way: create an instance directly from positional arguments
Vehicle memory car2 = Vehicle({make:make, model:model, year:year, owner:msg.sender}); // second way: pass arguments in a Python-dict-like structure to create the instance
Vehicle memory car3; // third way: create the instance first, then assign fields via dot notation
car3.make = make;
car3.model = model;
car3.year = year;
car3.owner = msg.sender;
carpark.push(car);
carpark.push(car2);
}
Complete example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Structs{
struct Vehicle{
string make;
string model;
uint year;
address owner;
}
// Vehicle public car;
// Vehicle public car2;
Vehicle[] public carpark;
function buy(string memory make, string memory model, uint year) external{
Vehicle memory car = Vehicle(make, model, year, msg.sender);
Vehicle memory car2 = Vehicle({make:make, model:model, year:year, owner:msg.sender});
Vehicle memory car3;
car3.make = make;
car3.model = model;
car3.year = year;
car3.owner = msg.sender;
carpark.push(car);
carpark.push(car2);
}
function carInfo(uint index) external view returns(string memory, string memory, uint){
Vehicle memory _car = carpark[index];
return(_car.make, _car.model, _car.year);
}
function delCar(uint index) external{
carpark[index] = carpark[carpark.length-1];
carpark.pop();
}
function changeInfo(uint index, string memory make, string memory model, uint year) external{
carpark[index].make = make;
carpark[index].model = model;
carpark[index].year = year;
}
}
Enums
Defined with the enum keyword
enum OrderStatus{
None, // corresponds to 0
Pending, // 1
Shipped, // 2
Completed,
Rejected,
Cancelled
}
An enum is called an enum type. Once an enum type is defined, you can create instances of it
struct Order{
address buyer;
OrderStatus orderStatus;
}
Complete example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract OrderSystem{
enum OrderStatus{
None,
Pending,
Shipped,
Completed,
Rejected,
Cancelled
}
struct Order{
address buyer;
OrderStatus orderStatus;
}
Order[] private orders;
function newOrder(address buyer, OrderStatus orderStatus) public{
Order memory order = Order(buyer, orderStatus);
orders.push(order);
}
function changeStatus(uint index, OrderStatus orderStatus) public{
Order memory order = orders[index];
order.orderStatus = orderStatus;
orders[index] = order;
}
// function getStatus(uint index) public view returns(address, OrderStatus){
// Order memory order = orders[index];
// return (order.buyer, order.orderStatus);
// }
function getStatus(uint index) public view returns(Order memory){
Order memory order = orders[index];
return order;
}
}
Data Location
https://docs.soliditylang.org/zh-cn/v0.8.24/types.html#data-location-assignment storage, memory, calldata
For updating the value of a struct element inside an array,
// if you only change one or two fields, this is convenient and saves gas
tasks[index].completed = true;
Task memory _task = tasks[index];
_task.completed = true; // if you need to change many fields, first pull the element out of the array, then modify it — this saves more gas
_task.completed = true;
_task.completed = true;
_task.completed = true;
_task.completed = true;
tasks[index] = _task;
Events (event)
There are actually 4 topic slots, but one is occupied by default, so only three are available for the user to use. The first slot (index1) is the keccak256 hash of the event signature.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract MessageSys{
// define the event and specify indexed parameters
event LogMessage(address indexed sender, address indexed reveive, string indexed message);
// without indexing, everything except topic 0 goes into the topics list, while the remaining data goes into data
event noindex(address sender, address reveive, string message);
function sendMessage(address to, string calldata message) external {
emit LogMessage(msg.sender, to, message);
emit noindex(msg.sender, to, message);
}
}
Inheritance
For a function to be overridable, the method in the parent contract must be marked virtual.
Inheritance + Constructors
contract ContractC is ContractB("QFM"), ContractA{ // you can pass values to a parent contract at the point of inheritance
string public text_C;
constructor(string memory _text, string memory cc) ContractA(_text) { // when the child contract's constructor is called, declare which values go to the parent constructors
text_C = cc;
}
}
This form is probably more common — pass everything in when the child contract is created
contract ContractC is ContractB, ContractA{
string public text_C;
constructor(string memory _text, string memory cc, string memory bb) ContractA(_text) ContractB(bb) {
text_C = cc;
}
}
Complete example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract ContractA{
string public name;
constructor(string memory _name){
name = _name;
}
}
contract ContractB{
string public text;
constructor(string memory _text){
text = _text;
}
}
// contract ContractC is ContractB("QFM"), ContractA{
// string public text_C;
// constructor(string memory _text, string memory cc) ContractA(_text) {
// text_C = cc;
// }
// }
contract ContractC is ContractB, ContractA{
string public text_C;
constructor(string memory _text, string memory cc, string memory bb) ContractA(_text) ContractB(bb) {
text_C = cc;
}
}
Calling a Parent's Method
string memory tmp_b = super.too(); // using super calls the same-named method in the last inherited parent — "last" meaning the last parent contract listed in the inheritance declaration
or
string memory tmp_b = ContractB.too(); // explicitly call the method of a specific parent
Complete example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract ContractA{
uint public age = 20;
function foo() public pure returns(string memory){
return "foo A";
}
function too() public pure virtual returns(string memory){
return "too A";
}
}
contract ContractB is ContractA{
function too() public pure override virtual returns(string memory){
return "too B";
}
}
contract ContractC is ContractA{
function too() public pure override virtual returns(string memory){
return "too C";
}
}
contract ContractD is ContractB, ContractC{
function too() public pure override(ContractB, ContractC) returns(string memory){
return "too D";
}
function call() external pure returns(string memory, string memory) {
string memory tmp_b = ContractB.too();
string memory tmp_c = ContractC.too();
return (tmp_b, tmp_c);
}
function call2() external pure {
super.too();
}
}
Visibility
private: accessible only within the contract that defines them.
internal: accessible within the contract that defines them and its child contracts.
public: accessible both inside and outside the contract.
external: callable only from other contracts or external accounts. Cannot be applied to state variables.
private and virtual cannot be used together
function fu1() private virtual pure returns(uint){
return 2;
}
immutable
Definition and characteristics
- Can only be initialized at contract deployment
- Cannot be changed after initialization It effectively enriches the constant concept, allowing a variable to be assigned once during initialization and then act as a constant.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract A{
// 2492 gas
address public immutable i_owner; // conventionally prefixed with i_
// 358 gas
uint public constant MINIMUM_NUM = 100; // conventionally uppercase
constructor(){
i_owner = msg.sender;
}
function get() external view returns(address){
return i_owner;
}
}
Functions
Function Visibility
https://docs.soliditylang.org/zh-cn/v0.8.24/cheatsheet.html#index-10
public: visible both internally and externally
private: visible only within the current contract
internal: visible within the project, internally only (i.e. visible anywhere within the current Solidity source file, not limited to the current contract — translator's note)
external: for calls after deployment, visible only externally (can only be applied to functions) — that is, only usable via message calls (even when called within the contract, it must be done via this.func)
Constructors
A special method written inside a contract; a contract can have only one. Its general purpose is to set the contract's initial state. There isn't much to say about constructor — it's similar to Python's init. In other languages, when you create an object, there is a function with the same name as the object that is called once during the object's initialization; this is the constructor.
contract SimpleStorage{
uint public number;
constructor(uint x){
number = x;
}
}
Modifiers
https://docs.soliditylang.org/zh-cn/v0.8.24/cheatsheet.html#index-11
pure on a function: not allowed to modify or read state variables.
view on a function: not allowed to modify state variables.
payable on a function: allowed to receive Ether from the call.
constant on a state variable: assignment is not allowed (except at initialization); does not occupy a storage slot.
immutable on a state variable: allowed to be assigned at construction time and remains unchanged after deployment. Stored in the code.
anonymous on an event: the event signature is not stored as a topic.
indexed on an event parameter: stores the parameter as a topic.
virtual on functions and modifiers: allows the behavior of the function or modifier to be changed in derived contracts.
override indicates that the function, modifier, or public state variable changes the behavior of a function or modifier in a base contract.
Function Modifiers
A modifier works like a Python decorator: it attaches a piece of code/functionality to a method.
modifier whenNotPaused(){
assert(!paused);
_;
}
Function Return Values
To return multiple variables, use parentheses with comma-separated values (,,), similar to a tuple in Python. The common approach is an explicit return, writing out the return statement. With an implicit return, you write the names of the variables to return when defining the return types.
contract MultipleOutput{
address public a;
uint public b;
string public c;
address public d;
uint public e;
string public f;
function MultipleOut() private view returns(address, uint, string memory){
return(msg.sender, 234, "hello world");
}
// Implicit return: write the names of the variables to return in the function definition.
// Note the use of modifiers such as view/pure.
// view/pure are called Modifiers; the modifier keyword is called a function modifier.
function display() private view returns(address addr, uint num, string memory name){
// addr = address(0);
// addr = msg.sender;
addr = a;
num = 234;
name = "hello world";
}
function CapOut() external {
(a,b,c) = MultipleOut(); // note: multiple return values must be received with parentheses ()
// if you only need one of the values, you can use commas to skip the rest
// (a,,) = MultipleOut();
(d,e,f) = display();
}
}
Using new
Used to create contracts and fixed-length in-memory arrays.
Contracts
ContractName newInstance = new ContractName{value: <optional Wei>, gas: <optional Gas>}(constructor args...);
Complete example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Account{
address public owner;
constructor(address _owner) payable{ // adding payable allows it to receive eth
owner = _owner;
}
}
contract AccountFactory{
Account[] public accounts;
function createAccount(address owner, uint num) external payable { // creating an Account requires sending eth, so this is payable too
Account acc = new Account{value: num}(owner); // {} holds the parameters passed to the EVM, indicating num wei of eth is carried
accounts.push(acc);
}
}
Arrays
Can only exist in memory, and the length is fixed
Type[] memory arr = new Type[](length);
Example
function getArray(uint len) public pure returns (uint[] memory) {
uint[] memory arr = new uint[](len); // an array of length len
for (uint i = 0; i < len; i++) {
arr[i] = i + 1;
}
return arr;
}
Libraries (library)
Besides serving as a normal utility library of methods to enable code reuse, a library can also augment a data type so that it gains the methods defined in the library.
Note the visibility of methods in a library: only internal methods can be used to augment a type via the using xxx for xxx syntax. public and external methods require the library to be deployed separately.
Usage
Math.min, Math.max, etc. — call directly as Library.method
using ArrayLib for uint[] — augmentation-style usage
Practical Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
library MathLib{
function min(uint x, uint y) external pure returns(uint){
return x < y ? x:y;
}
}
library ArrayUtils{
function sum(uint[] memory array) external pure returns(uint){
uint tmp;
for (uint i =0; i< array.length; i++){
tmp += array[i];
}
return tmp;
}
}
contract TestLibrarys{
// using MathLib for uint;
using ArrayUtils for uint[];
uint[] public arr = [1,2,3,4,5,6,7,8,9];
function getMin(uint x, uint y) public pure returns(uint){
return MathLib.min(x, y);
}
function getSum() public view returns(uint){
return arr.sum();
}
}