LEARN X · ЗА 17 МИН
Solidity
Solidity за 17 минут: смарт-контракты Ethereum на одной странице — типы, функции, модификаторы, mapping, события, наследование, газ и безопасность.
Solidity — статически типизированный язык для смарт-контрактов виртуальной машины Ethereum (EVM). Код компилируется в байткод, разворачивается в блокчейн и исполняется детерминированно. Весь язык — в комментариях рабочего кода (компилятор 0.8+).
Структура контракта
// SPDX-License-Identifier: MIT
// Первая строка — лицензия (SPDX), иначе предупреждение компилятора.
// pragma задаёт версию компилятора: ^0.8.0 — от 0.8.0 до <0.9.0.
pragma solidity ^0.8.0;
// Импорт другого файла/контракта (как модули).
// import "./OtherContract.sol";
// contract — основная единица, похож на class.
contract Hello {
// Тело контракта: переменные состояния, функции, события...
string public greeting = "Привет, мир"; // строчный комментарий
/* блочный
комментарий */
}
Типы данных
contract Types {
// Целые без знака: uint8..uint256 шагом 8. uint == uint256.
uint256 count = 42;
uint8 small = 255; // максимум для 8 бит
// Целые со знаком.
int256 temperature = -10;
// Логический.
bool isReady = true;
// Адрес (20 байт). address payable умеет принимать ETH.
address owner = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
address payable wallet;
// Байтовые типы фиксированной длины: bytes1..bytes32.
bytes32 hash;
// Динамические: bytes и string (хранятся в storage/memory).
string name = "codechick";
bytes data;
// Целочисленное деление округляет вниз, дробных типов нет.
uint256 half = 7 / 2; // == 3
}
Переменные состояния и видимость
Переменные состояния хранятся в блокчейне (storage) и стоят газа при записи.
contract Storage {
// public — компилятор создаёт авто-геттер с тем же именем.
uint256 public total;
// private — доступ только из этого контракта (но данные видны в блокчейне!).
uint256 private secret;
// internal — этот контракт и наследники (по умолчанию для переменных).
uint256 internal shared;
// constant — известно при компиляции, дешевле по газу.
uint256 public constant MAX_SUPPLY = 1000000;
// immutable — задаётся один раз в конструкторе, потом не меняется.
address public immutable creator;
constructor() {
creator = msg.sender;
}
}
Функции
contract Functions {
uint256 public value;
// Параметры с типами, видимость, возвращаемый тип через returns.
function add(uint256 a, uint256 b) public pure returns (uint256) {
return a + b; // pure — не читает и не пишет состояние
}
// view — читает состояние, но не изменяет его.
function getValue() public view returns (uint256) {
return value;
}
// Меняет состояние — не помечается view/pure (стоит газа).
function setValue(uint256 v) public {
value = v;
}
// Можно вернуть несколько значений (кортеж).
function minMax(uint256 a, uint256 b) public pure returns (uint256 lo, uint256 hi) {
return a < b ? (a, b) : (b, a);
}
}
Видимость и mutability функций
contract Visibility {
// public — вызывается извне и изнутри.
function p() public {}
// external — только извне (через this.f() изнутри); дешевле для больших аргументов.
function e() external {}
// internal — этот контракт и наследники.
function i() internal {}
// private — только этот контракт.
function pr() private {}
// payable — функция может принимать ETH вместе с вызовом.
function deposit() public payable {
// msg.value — сколько wei прислали
}
// Сводка mutability:
// pure — не трогает состояние
// view — только читает
// (ничего)— читает и пишет
// payable — ещё и принимает ETH
}
Конструктор
contract Owned {
address public owner;
uint256 public createdAt;
// constructor выполняется один раз при деплое и не хранится в блокчейне.
constructor(uint256 _start) {
owner = msg.sender; // тот, кто развернул контракт
createdAt = block.timestamp;
// _start — аргумент деплоя
}
}
Маппинги и массивы
contract Collections {
// mapping(ключ => значение): хеш-таблица, все ключи как бы существуют со значением 0.
mapping(address => uint256) public balances;
// Вложенный mapping: владелец -> (оператор -> разрешено?).
mapping(address => mapping(address => bool)) public approved;
// Динамический массив.
uint256[] public numbers;
// Массив фиксированной длины.
uint256[3] public triple;
function demo() public {
balances[msg.sender] = 100; // запись по ключу
numbers.push(7); // добавить в конец
uint256 len = numbers.length; // длина
numbers.pop(); // удалить последний
// У mapping нельзя узнать длину или перебрать ключи!
}
}
Структуры и перечисления
contract Models {
// struct — пользовательский составной тип.
struct User {
string name;
uint256 balance;
bool active;
}
// enum — именованные константы (0, 1, 2...).
enum Status { Pending, Active, Closed }
Status public status = Status.Pending;
mapping(address => User) public users;
function register(string memory _name) public {
// Создаём структуру по именам полей.
users[msg.sender] = User({name: _name, balance: 0, active: true});
status = Status.Active;
}
}
Модификаторы, require и revert
contract Guards {
address public owner;
constructor() { owner = msg.sender; }
// modifier — переиспользуемая проверка; _ подставляет тело функции.
modifier onlyOwner() {
require(msg.sender == owner, "Not owner"); // условие + сообщение
_;
}
// Кастомные ошибки (дешевле строк по газу, с 0.8.4).
error InsufficientBalance(uint256 available, uint256 required);
function withdraw(uint256 amount) public onlyOwner {
uint256 bal = address(this).balance;
// revert откатывает все изменения транзакции.
if (amount > bal) revert InsufficientBalance(bal, amount);
// require — то же самое для простых проверок.
require(amount > 0, "Zero amount");
// assert — для инвариантов, которые "никогда" не должны нарушаться.
assert(bal >= amount);
}
}
События
События пишутся в журнал транзакции — дёшево и удобно слушать с фронтенда.
contract Events {
// indexed-поля (до 3) позволяют фильтровать события по значению.
event Transfer(address indexed from, address indexed to, uint256 value);
event Log(string message);
mapping(address => uint256) public balance;
function send(address to, uint256 amount) public {
balance[msg.sender] -= amount;
balance[to] += amount;
// emit вызывает событие.
emit Transfer(msg.sender, to, amount);
emit Log("transfer done");
}
}
Наследование
// Базовый контракт.
contract Animal {
// virtual — метод можно переопределить в наследнике.
function sound() public pure virtual returns (string memory) {
return "...";
}
}
// is — наследование (поддерживается множественное: is A, B).
contract Dog is Animal {
// override — переопределяем виртуальный метод.
function sound() public pure override returns (string memory) {
return "Гав";
}
}
// Абстрактный контракт и интерфейс.
interface IToken {
// В interface все функции external и без тела.
function transfer(address to, uint256 amount) external returns (bool);
}
Глобальный объект msg и контекст
contract Context {
// msg.* — данные текущего вызова.
function inspect() public payable returns (address, uint256) {
address caller = msg.sender; // кто вызвал (адрес)
uint256 sent = msg.value; // сколько wei прислали
bytes memory raw = msg.data; // сырые calldata
raw; // (чтобы не было предупреждения)
// block.* и tx.* — данные блока и транзакции.
uint256 ts = block.timestamp; // время блока
uint256 num = block.number; // номер блока
ts; num;
return (caller, sent);
}
// Получить ETH контрактом — нужна receive или fallback.
receive() external payable {}
fallback() external payable {}
}
Газ и безопасность
Каждая операция стоит газ; транзакция откатывается, если газ кончился. Главный класс уязвимостей — reentrancy.
contract Safety {
mapping(address => uint256) public balances;
// ОПАСНО: внешний вызов до обновления состояния — атака повторного входа.
function badWithdraw() public {
uint256 amount = balances[msg.sender];
(bool ok, ) = msg.sender.call{value: amount}(""); // вызовет fallback атакующего
require(ok);
balances[msg.sender] = 0; // обновили СЛИШКОМ поздно
}
// ПРАВИЛЬНО: "checks-effects-interactions" — сперва меняем состояние, потом перевод.
function goodWithdraw() public {
uint256 amount = balances[msg.sender];
require(amount > 0, "empty");
balances[msg.sender] = 0; // effect до interaction
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "transfer failed");
}
// В 0.8+ арифметика проверяет переполнение автоматически (revert при overflow).
}
Пример: контракт-хранилище с балансами
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// Простой банк: вносим и выводим ETH, владелец может смотреть казну.
contract Bank {
address public immutable owner;
mapping(address => uint256) private balances;
event Deposited(address indexed who, uint256 amount);
event Withdrawn(address indexed who, uint256 amount);
modifier onlyOwner() {
require(msg.sender == owner, "only owner");
_;
}
constructor() {
owner = msg.sender;
}
// Вносим ETH на свой счёт.
function deposit() external payable {
require(msg.value > 0, "send some ETH");
balances[msg.sender] += msg.value;
emit Deposited(msg.sender, msg.value);
}
// Выводим со своего счёта (безопасный порядок действий).
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount, "not enough");
balances[msg.sender] -= amount; // сначала эффект
(bool ok, ) = payable(msg.sender).call{value: amount}("");
require(ok, "transfer failed"); // потом перевод
emit Withdrawn(msg.sender, amount);
}
// Свой баланс.
function myBalance() external view returns (uint256) {
return balances[msg.sender];
}
// Вся казна контракта — только владелец.
function treasury() external view onlyOwner returns (uint256) {
return address(this).balance;
}
}