Перейти к основному содержимому
Блокчейн07ГЛАВА 07Функции: видимость, view, payable и модификаторыpublic · viewmodifier _;телокто и почём

Функции: видимость, view, payable и модификаторы

Прочитано0%
Квизы0/5
Тренажёрыне пройден0/1
Уровень 1 · Новичок

Зачем это нужно​

Одна строка из эталонного контракта задания:

function getUserData(address _wallet) public view AccessControl(Role.Owner) returns (User memory)

Здесь пять решений, и каждое меняет поведение: кто может вызвать, платит ли вызывающий, что проверяется до входа в тело, что возвращается наружу. Прочитать такую строку — значит понять функцию, не читая её тела. Это и есть содержание главы.

Заодно выяснится, что private не означает «секретно», что слово view экономит вызывающему все деньги до копейки, а два способа отправить эфир на один и тот же адрес дают разный результат: один проходит, другой откатывает всю транзакцию.

Функция — кусок кода с именем, входом и выходом​

function addPure(uint256 a, uint256 b) public pure returns (uint256) {
return a + b;
}
Нажми на часть выражения

Параметр — это переменная, которая живёт только во время вызова. Вызов addPure(2, 3) кладёт двойку в a, тройку в b, выполняет тело и возвращает пятёрку.

Возвращать можно несколько значений сразу — кортежем. Так сделано в эталонном контракте:

function getBalance() public view returns (uint256, uint256, uint256, uint256) {
return (msg.sender.balance, userMap[msg.sender].seedTokens,
userMap[msg.sender].privateTokens, userMap[msg.sender].publicTokens);
}

Четыре числа за один вызов — вместо четырёх отдельных функций.

Видимость: кто имеет право вызвать​

Четыре слова, и выбрать нужно всегда — умолчания нет.

СловоКто может вызвать
publicвсе: снаружи, изнутри контракта, из контракта-наследника
externalтолько снаружи; изнутри — лишь через this.f()
internalтолько сам контракт и его наследники
privateтолько сам контракт

Наследник — контракт, который берёт содержимое другого и дописывает своё; этим займёмся в отдельной главе.

Первая модель разворачивает контракт с пятью функциями и смотрит, что от него видно снаружи:

── что попало в ABI (список функций, видимых снаружи) ──
Visibility: callHidden(), onlyOutside(), openForAll()
всего функций в контракте: 5, в ABI: 3

── вызовы снаружи, из скрипта ──
openForAll() → 42
onlyOutside() → 42
callHidden() → 84 (internal + private сложились ВНУТРИ)
c.onlyInside — есть ли такое поле у объекта? → undefined
c.onlyHere — есть ли такое поле у объекта? → undefined
попытка вызвать c.onlyHere() из скрипта → c.onlyHere is not a function

── наследник Child ──
useParent() → 42 (internal родителя виден наследнику)
ABI Child: callHidden(), onlyOutside(), openForAll(), useParent()

── байткод: имя функции в цепь не попадает, только 4 байта селектора ──
openForAll 0xc9dc5bc6
onlyOutside 0xb5964cbb
callHidden 0x155f1206
ПЯТЬ ФУНКЦИЙ В КОНТРАКТЕ, ТРИ — СНАРУЖИконтрактopenForAll publiconlyOutside externalcallHidden publiconlyInside internalonlyHere privateABIчто видит скрипт и кошелёкopenForAll() · onlyOutside() · callHidden()c.onlyHere → undefinedc.onlyHere() → c.onlyHere is not a functioncallHidden() вернул 84: внутренние функции отработали — просто внутриprivate не значит «секретно»: байткод публичен, скрыт только входимени функции в цепи нет — только 4 байта селектора: 0xc9dc5bc6
Функций пять, в ABI три: внутренние работают, но снаружи их не существует

Внутренние функции никуда не делись — callHidden() вернул 84, значит обе отработали. Просто снаружи их нет в ABI, и скрипт не видит у объекта таких полей.

Важно

private не значит «секретно». Байткод контракта публичен, и тело приватной функции в нём читается. Скрыт только вход: вызвать снаружи нельзя. Хранить в приватной переменной пароль или ключ — то же самое, что написать его на заборе; в отчёте по информационной безопасности это отдельный пункт.

view и pure: обещания, которые проверяет компилятор​

view — функция читает состояние, но не меняет. pure — не читает и не меняет вовсе.

Это не подсказки для читателя, а обещания компилятору. Нарушил — сборка не пройдёт: запись внутри view-функции даёт TypeError: Function cannot be declared as view because this expression (potentially) modifies the state.

Зачем обещание нужно? Вторая модель отвечает деньгами:

── pure и view: вызов (eth_call), не транзакция ──
addPure(2, 3) → 5
addView(5) → 5 (counter пока 0)
counter после двух вызовов: 0
списано с кошелька за оба вызова: 0 wei

── addWrite: транзакция, за неё платят ──
1-я запись (0 → 5): gasUsed = 44230
2-я запись (5 → 10): gasUsed = 27130
разница: 17100 газа — первая запись в пустую ячейку дороже
counter теперь: 10

── что вернула транзакция в скрипт? ──
tx2 — это объект транзакции, а не число. Поле hash: 0x5470a1c1…
returns у пишущей функции снаружи НЕ читается; читают counter(): 10
но то же значение можно получить без транзакции:
addWrite.staticCall(5) → 15 (симуляция, counter не изменился: 10)

── цена того же вызова, если бы addView была транзакцией ──
estimateGas(addView(5)) = 24138 — столько бы стоило, отправь её транзакцией
из них 21000 — базовая цена любой транзакции
ОДНА И ТА ЖЕ АРИФМЕТИКА: 0 WEI ИЛИ 44 230 ГАЗАpure / viewузел считает у себя и отвечаетсписано: 0 weiблоков не создаётся, ответ сразуфункция без view — транзакция1-я запись (0 → 5): 44 230 газа2-я запись (5 → 10): 27 130 газаразница 17 100 — цена заполнения нуляпишущая функция возвращает наружу не число, а объект транзакции: результат читают отдельным вызовомесли бы то же чтение отправили транзакцией, оно стоило бы 24 138 газа — из них 21 000 базовая ценаview — обещание компилятору: запись внутри такой функции не соберётся
Слово view превращает вызов в бесплатный: узел считает у себя, блок не создаётся

Три вывода.

Чтение бесплатно. Пометив функцию view, вы говорите сети: её можно выполнить локально. Узел считает у себя и отвечает — с кошелька не списывается ничего.

Возврат пишущей функции снаружи не прочитать. Транзакция отдаёт объект с хешем, а не число: пока она не попала в блок, результата ещё не существует. Значение читают отдельным view-вызовом. Отсюда привычка: почти к каждому изменению в контракте прилагается функция чтения.

Разница 17 100 газа между первой и второй записью — уже знакомая цена заполнения нулевой ячейки.

Отвечено вопросов: 0 из 2
Пять функций в контракте, три в ABI. Что стало с остальными двумя?
Функция помечена view. Сколько заплатит вызывающий?
Отвечено верно: 0 из 2

payable: функция, которая принимает деньги​

По умолчанию функция денег не принимает. Транзакция с эфиром в обычную функцию отклоняется ещё до тела, а обращение к сумме внутри неё не компилируется:

TypeError: "msg.value" and "callvalue()" can only be used in payable public functions.

Слово payable это разрешает, а msg.value говорит, сколько пришло:

function buyToken(uint256 _amount) public payable {
uint256 price = getTokenPrice();
require(msg.value >= _amount * price, unicode"Недостаточно эфира для покупки");
payable(owner).transfer(msg.value);
}

msg.value измеряется в wei — как всегда, целым числом. Проверять его нужно самому: сеть не следит за тем, чтобы прислали ровно столько, сколько стоит покупка.

Два способа отправить эфир дают разный результат​

Отправить эфир можно несколькими способами, и выбор между ними — не вкусовщина. Четвёртая модель отправляет один wei одному и тому же получателю двумя способами:

transfer: Transaction reverted: contract call run out of gas and made the transaction revert
call ok, gasUsed = 53842 count = 1
ОДИН ПОЛУЧАТЕЛЬ, ОДИН WEI, ДВА СПОСОБА ОТПРАВИТЬpayable(to).transfer(1)получателю отдаётся ровно 2300 газаcontract call run out of gasвся транзакция откатиласьto.call{value: 1}("")газ не ограничен искусственнопрошло за 53 842 газаполучатель записал у себя count = 1получатель — контракт, который при приёме пишет в хранилище: в 2300 газа такая запись не помещаетсяпо тексту двух функций разницы не видно — видно только по результату
Один wei, один получатель: transfer откатил транзакцию, call прошёл

Получатель здесь — контракт, который при приёме денег делает запись у себя. transfer передаёт получателю жёстко 2300 газа — и записи в хранилище в эти 2300 не помещается, поэтому вся транзакция откатывается. call такого ограничения не ставит: перевод прошёл за 53 842 газа, получатель записал единицу.

Правильная запись через call:

(bool ok, ) = payable(to).call{value: amount}("");
require(ok, unicode"Перевод не прошёл");

Проверка ok обязательна: call не откатывает транзакцию сам, он возвращает признак успеха. Забыли проверить — контракт считает перевод выполненным, хотя денег никто не получил.

Интересный факт

Именно поэтому документация помечает transfer и send как устаревшие. В эталонном контракте задания стоит payable(owner).transfer(...) — и это готовый пункт для отчёта по информационной безопасности модуля В: «получатель-контракт со своей логикой приёма не сможет получить средства; заменить на call с проверкой результата».

Модификатор: проверка, вынесенная за скобки​

Когда одна и та же проверка нужна в десяти функциях, её выносят в модификатор:

modifier AccessControl(Role _role) {
require(userMap[msg.sender].role == _role, unicode"Недостаточно прав");
_;
}

function stopPublicPhase() public AccessControl(Role.Owner) { … }
function giveReward(address to, uint256 amount) public AccessControl(Role.publicProvider) { … }
Нажми на часть выражения

Третья модель показывает порядок выполнения, который в исходнике не виден:

── владелец назначает alice роль Provider (onlyRole → traced → тело) ──
шаг 1: модификатор: до тела
шаг 2: тело setRole
шаг 3: модификатор: после тела
step в хранилище: 3

── alice вызывает provide() ──
шаг 4: модификатор: до тела
шаг 5: тело provide
шаг 6: модификатор: после тела
step в хранилище: 6

── владелец вызывает provide(), но роль не Provider ──
отказ: reverted with reason string 'Недостаточно прав'
step в хранилище после отказа: 6 — require упал ДО «_;»,
тело и traced не запускались

── alice пытается назначить роль сама себе ──
отказ: reverted with reason string 'Недостаточно прав'
роль alice по-прежнему: Provider
«_;» — ЭТО МЕСТО, КУДА ВСТАВЛЯЕТСЯ ТЕЛО ФУНКЦИИmodifier onlyRole(Role r) {require(roles[msg.sender] == r, …);шаг 1: до тела_; ← сюда встаёт шаг 2: тело функциишаг 3: после тела}живая трасса вызовашаг 1 · шаг 2 · шаг 3при неверной роли:отказ, счётчик не сдвинулсяrequire упал ДО «_;»код после «_;» действительно выполняется — модификатор оборачивает функцию, а не только предваряет еёнесколько модификаторов срабатывают слева направо; отказ в любом откатывает всю транзакцию
«_;» — буквально место вставки тела: код после него тоже выполняется, уже после функции

Модификатор оборачивает функцию, а не просто предваряет её: строки до _; идут перед телом, строки после — после тела. Это пригодится в защите от повторного входа, которую разберём в главе про паттерны безопасности.

Модификаторов может быть несколько — тогда они срабатывают слева направо, и отказ в любом откатывает всю транзакцию.

Отвечено вопросов: 0 из 2
Что означает строка «_;» внутри модификатора?
Перевод одного wei контракту-получателю: transfer откатил транзакцию, call прошёл. Почему?
Отвечено верно: 0 из 2

Читаем строку из эталонного контракта​

Теперь та самая строка целиком:

function getUserData(address _wallet) public view AccessControl(Role.Owner) returns (User memory)
  • getUserData — имя; (address _wallet) — принимает адрес.
  • public — вызвать может кто угодно… но
  • AccessControl(Role.Owner) — модификатор пропустит только владельца. Видимость и права — разные вещи: public говорит «функция есть в ABI», модификатор — «но сработает не у всех».
  • view — только читает, значит вызов бесплатный.
  • returns (User memory) — возвращает целую структуру. Слово memory означает «копия на время вызова»; про места хранения — в следующей главе.

Одна строка, прочитанная за десять секунд, рассказывает о функции больше, чем её тело.

Типичные ошибки​

Забыта видимость. SyntaxError: No visibility specified. Did you intend to add "public"? — слово обязательно.

Строковый параметр без места хранения. TypeError: Data location must be "memory" or "calldata" for parameter in function, but none was given.

Запись внутри view. TypeError: Function cannot be declared as view because this expression (potentially) modifies the state.

msg.value в функции без payable. TypeError: "msg.value" and "callvalue()" can only be used in payable public functions.

transfer на обычном address. TypeError: "send" and "transfer" are only available for objects of type "address payable", not "address".

Результат call не проверен. Компилируется, но перевод может тихо не пройти: require(ok, …) обязателен.

Модификатор без _;. SyntaxError: Modifier body does not contain "_".

Ожидание возвращаемого значения от пишущей функции. Снаружи придёт объект транзакции; значение читают отдельным view-вызовом.

Перегрузка с неоднозначным вызовом. Две функции с одним именем и близкими типами: TypeError: No unique declaration found after argument-dependent lookup.

Слова главы​

  • Функция (function) — именованный код с параметрами и возвратом.
  • Параметр — переменная, живущая только во время вызова.
  • returns — что функция отдаёт; несколько значений возвращают кортежем.
  • Видимость: public — все, external — только снаружи, internal — контракт и наследники, private — только контракт.
  • ABI — список функций, видимых снаружи; internal и private в него не попадают.
  • Селектор — 4 байта, по которым сеть находит функцию; имени в цепи нет.
  • view — только читает; pure — не читает и не меняет. Вызов бесплатен.
  • payable — функция принимает эфир; msg.value — сколько пришло, в wei.
  • transfer / call — два способа отправить эфир; у transfer жёсткие 2300 газа.
  • Модификатор (modifier) — обёртка над функцией; _; — место вставки тела.
  • Перегрузка — одно имя, разные наборы параметров; вызов должен быть однозначным.

Тренажёр: набрать вслепую​

Цель: скорость ≥ 100 зн/мин, точность ≥ 88%

function getUserData(address _wallet) public view returns (User memory) {

На чемпионате​

Модуль А, 37 баллов. Перечень функций в задании расписан по пунктам: deposit, withdraw, supply, borrow, repay, ликвидация — и у каждой перечислены условия. Практический перевод на язык этой главы:

  • каждое условие — require с понятным текстом в начале функции;
  • функции, принимающие деньги, помечены payable;
  • функции чтения для дашборда — view, иначе интерфейс будет требовать транзакцию за каждый показ метрики;
  • права — модификатором, а не копией проверки в каждой функции.

Отдельное требование задания: «привести достаточные пояснения к работе всех функций смарт-контракта, описание входных параметров и возвращаемых значений». Комментарий над каждой функцией — что делает, что принимает, что возвращает.

Модуль В, 14 баллов. Готовые пункты отчётов из этой главы: transfer вместо call (информационная безопасность), пропущенная проверка результата перевода, функция без модификатора прав там, где он нужен, и функция чтения без view — последнее идёт в отчёт по оптимизации: каждый показ метрики стоил бы пользователю газа.

Отвечено вопросов: 0 из 2
Функция дашборда возвращает метрику, но не помечена view. Что не так?
Функция помечена public, но с модификатором прав. Противоречие?
Отвечено верно: 0 из 2

Что дальше​

Функции научились принимать данные и отдавать их. Осталось разобраться, где эти данные лежат: чем хранилище отличается от памяти на время вызова, как устроены отображения и структуры, почему delete в массиве оставляет дыру вместо того, чтобы его укоротить, и как контракт пишет журнал событий для внешнего мира. Это следующая глава — и последняя в блоке про основы Solidity.

Отвечено вопросов: 0 из 1
Почему возврат пишущей функции нельзя прочитать снаружи?
Отвечено верно: 0 из 1

Углубиться​

  • calldata вместо memory — параметр читается прямо из данных транзакции без копирования; на длинных строках и массивах экономит заметно.
  • Именованные возвращаемые значения — returns (uint256 total): присвоил переменной и можно писать return без аргументов.
  • this.f() — вызов собственной функции «снаружи»; нужен для external, стоит дороже обычного.
  • Функции получения эфира — receive() и fallback(): что вызывается, когда деньги приходят без указания функции.
  • Перегрузка и селекторы — почему у f(uint256) и f(uint8) разные четыре байта и как это связано с ABI.

Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.

Куда дальше: проверенные ресурсы​

Видео по теме

ВидеоSolidity и смарт-контракты Ethereum, урок #6 | Функции, транзакции, pure, view, Ilya Krukowski
Видео90% новичков путают view, pure и payable — разбор на примерахGuide DAO
ВидеоУчебник по функциям Solidity | Область видимости, параметры и возвращаемые значеConflux Russian
ВидеоSolidity | Урок 12. Модификаторы функцийАртем Кукуруза
ВидеоSolidity Functions Full Guide | Return Multiple Values, Named Returns & Array Inthebasicsdotacademy

Комментарии

Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.