Что здесь происходит
Я Райт. Учу web3-инженерии и строю её у вас на глазах:
⚒️ Blockwright Wallet — некастодиальный кошелёк, пишу с нуля. BTC уже в релизе, ETH — на этой неделе
⚒️ Blockwright Academy — дерево навыков для JS-разработчиков, строится в открытую
⚒️ blockwright.dev — сайт проекта, только что запущен — загляните
Рубрики: серия «с нуля до web3» — вт и чт · «Словарь Райта» — пн, ср, пт
Сайт: blockwright.dev
EN-версия стройки: x.com/blockwright_dev
Статьи: habr.com/ru/users/Blockwright
Я Райт. Учу web3-инженерии и строю её у вас на глазах:
⚒️ Blockwright Wallet — некастодиальный кошелёк, пишу с нуля. BTC уже в релизе, ETH — на этой неделе
⚒️ Blockwright Academy — дерево навыков для JS-разработчиков, строится в открытую
⚒️ blockwright.dev — сайт проекта, только что запущен — загляните
Рубрики: серия «с нуля до web3» — вт и чт · «Словарь Райта» — пн, ср, пт
Сайт: blockwright.dev
EN-версия стройки: x.com/blockwright_dev
Статьи: habr.com/ru/users/Blockwright
Помнишь пост 10? Мы добавили в Копилку
Тикер против подписки. В посте 13 мы сделали реактивность через «новый блок → перечитай всё». Это работает, но это опрос: страница каждые 12 секунд переспрашивает «а что там?». Знакомо по web2 - так выглядит polling. А события - это webhook: не спрашивай, тебе скажут. Причём скажут с подробностями:
Тикер из поста 13 отвечал на вопрос «изменилось ли что-то?». Событие отвечает на вопрос «что именно произошло?» - кто положил, сколько, и каким стал баланс. Разница как между «в базе что-то поменялось» и нормальным pub/sub
Живая лента за 15 строк. Складываем приходящие события в стейт - и у Копилки появляется лента депозитов, как лента переводов в банковском приложении:
Заметь: никакого бэкенда, который агрегирует переводы, никакой базы, никакого API для ленты. История уже лежит в блокчейне - мы просто подписались на её хвост
Этим постом закрывается блок 3. Посмотри, что у нас в руках: контракт с событиями и кастомными ошибками, семь тестов на node:test, фронт с кошельком, чтением, записью и живой лентой. Это не учебные кусочки - это полный dApp, такой же по строению, как Uniswap, только маленький
Задание №15: добавь ленту, открой страницу в двух окнах - и сделай депозит из одного. Второе узнает само. А если хочется магии погуще - импортируй в MetaMask второй аккаунт из hardhat node и закинь депозит «от чужого имени»: лента покажет другой адрес
В следующем посте начнём блок 4 - и начнём с преступления: мы нашу Копилку взломаем. По-настоящему
Райт · blockwright.eth
github.com/blockwright/kopilka · tag post-15
emit Deposited(...) и заплатили за него ~1 700 газа. Я тогда сказал: событие - это крик в журнал, который контракт сам никогда не прочитает. Сегодня выясняется, для кого мы кричалиТикер против подписки. В посте 13 мы сделали реактивность через «новый блок → перечитай всё». Это работает, но это опрос: страница каждые 12 секунд переспрашивает «а что там?». Знакомо по web2 - так выглядит polling. А события - это webhook: не спрашивай, тебе скажут. Причём скажут с подробностями:
useWatchContractEvent({
abi: kopilkaAbi,
address: KOPILKA_ADDRESS,
eventName: "Deposited",
onLogs(logs) {
// кто, сколько, и каким стал баланс — прямо в аргументах
logs.forEach((log) =>
console.log(log.args.from, log.args.amount, log.args.total),
);
},
});Тикер из поста 13 отвечал на вопрос «изменилось ли что-то?». Событие отвечает на вопрос «что именно произошло?» - кто положил, сколько, и каким стал баланс. Разница как между «в базе что-то поменялось» и нормальным pub/sub
Живая лента за 15 строк. Складываем приходящие события в стейт - и у Копилки появляется лента депозитов, как лента переводов в банковском приложении:
const [feed, setFeed] = useState<string[]>([]);
// в onLogs:
setFeed((prev) => [
...logs.map((log) =>
`+${formatEther(log.args.amount ?? 0n)} ETH от ${log.args.from?.slice(0, 8)}…`),
...prev,
].slice(0, 5));
Заметь: никакого бэкенда, который агрегирует переводы, никакой базы, никакого API для ленты. История уже лежит в блокчейне - мы просто подписались на её хвост
Этим постом закрывается блок 3. Посмотри, что у нас в руках: контракт с событиями и кастомными ошибками, семь тестов на node:test, фронт с кошельком, чтением, записью и живой лентой. Это не учебные кусочки - это полный dApp, такой же по строению, как Uniswap, только маленький
Задание №15: добавь ленту, открой страницу в двух окнах - и сделай депозит из одного. Второе узнает само. А если хочется магии погуще - импортируй в MetaMask второй аккаунт из hardhat node и закинь депозит «от чужого имени»: лента покажет другой адрес
В следующем посте начнём блок 4 - и начнём с преступления: мы нашу Копилку взломаем. По-настоящему
Райт · blockwright.eth
github.com/blockwright/kopilka · tag post-15
#словарь_райта · №19 - EVM
Ethereum Virtual Machine. Если писал на Java или C# - модель знакома: код компилируется не в машинные инструкции процессора, а в байткод для виртуальной машины, и этот байткод одинаково работает где угодно
Solidity устроен так же: компилятор превращает нашу Копилку в байткод EVM, и каждый узел сети исполняет его на своей копии виртуальной машины. Write once, run everywhere - только «everywhere» здесь означает «на тысячах чужих компьютеров одновременно, с обязательным байт-в-байт одинаковым результатом»
Два отличия от JVM, оба уже знакомых по серии: каждая инструкция стоит газа (счётчик, который нельзя отключить), и вся машина детерминирована - нет ни случайности, ни часов, ни сети. EVM - это JVM, посаженная на строгую диету ради консенсуса
Зачем тебе это слово: «EVM-совместимый» в описании сети означает «твой контракт заработает там без переписывания». Это крупнейшая экосистема в web3 - и причина, почему Solidity стоит учить
Райт · blockwright.eth
Ethereum Virtual Machine. Если писал на Java или C# - модель знакома: код компилируется не в машинные инструкции процессора, а в байткод для виртуальной машины, и этот байткод одинаково работает где угодно
Solidity устроен так же: компилятор превращает нашу Копилку в байткод EVM, и каждый узел сети исполняет его на своей копии виртуальной машины. Write once, run everywhere - только «everywhere» здесь означает «на тысячах чужих компьютеров одновременно, с обязательным байт-в-байт одинаковым результатом»
Два отличия от JVM, оба уже знакомых по серии: каждая инструкция стоит газа (счётчик, который нельзя отключить), и вся машина детерминирована - нет ни случайности, ни часов, ни сети. EVM - это JVM, посаженная на строгую диету ради консенсуса
Зачем тебе это слово: «EVM-совместимый» в описании сети означает «твой контракт заработает там без переписывания». Это крупнейшая экосистема в web3 - и причина, почему Solidity стоит учить
Райт · blockwright.eth
#словарь_райта · №20 - Rollup
Классика бэкенда: тысяча мелких INSERT'ов убивает базу - собери их в батч и запиши одним запросом. Rollup - тот же приём, применённый к целому блокчейну
Ethereum дорог и нетороплив, потому что каждую транзакцию проверяют все узлы. Rollup выносит исполнение наружу: отдельная сеть (Arbitrum, Optimism, Base) обрабатывает сотни транзакций у себя, а в Ethereum публикует сжатую пачку плюс доказательство, что всё посчитано честно. Ethereum превращается из исполнителя в нотариуса: не переделывает работу, а заверяет итог
Отсюда «L2» - второй уровень (layer 2) поверх первого. Комиссии в десятки раз ниже, безопасность наследуется от Ethereum: даже если rollup-сеть исчезнет, твои средства восстановимы из опубликованных на L1 данных
Зачем тебе это слово: реальные продукты в 2026 живут в основном на L2 - деплоить Копилку «в прод» мы будем именно туда. Батчинг, который ты делал для Postgres, оказался архитектурой на миллиарды долларов
Райт · blockwright.eth
Классика бэкенда: тысяча мелких INSERT'ов убивает базу - собери их в батч и запиши одним запросом. Rollup - тот же приём, применённый к целому блокчейну
Ethereum дорог и нетороплив, потому что каждую транзакцию проверяют все узлы. Rollup выносит исполнение наружу: отдельная сеть (Arbitrum, Optimism, Base) обрабатывает сотни транзакций у себя, а в Ethereum публикует сжатую пачку плюс доказательство, что всё посчитано честно. Ethereum превращается из исполнителя в нотариуса: не переделывает работу, а заверяет итог
Отсюда «L2» - второй уровень (layer 2) поверх первого. Комиссии в десятки раз ниже, безопасность наследуется от Ethereum: даже если rollup-сеть исчезнет, твои средства восстановимы из опубликованных на L1 данных
Зачем тебе это слово: реальные продукты в 2026 живут в основном на L2 - деплоить Копилку «в прод» мы будем именно туда. Батчинг, который ты делал для Postgres, оказался архитектурой на миллиарды долларов
Райт · blockwright.eth
Обещал - делаю. Сегодня мы ограбим собственный контракт, вытащим из него все деньги одной транзакцией - и ты увидишь самую знаменитую уязвимость Ethereum своими глазами. Это словарь №12, реентерабельность, только теперь вживую
Добавим человечную фичу Копилка копит на цель. Логично разрешить передумать: пока цель не достигнута -забери свой вклад обратно. Для этого контракт запоминает, кто сколько внёс, и отдаёт по запросу:
Выглядит безобидно. Читается сверху вниз, как обычный JS. И здесь спрятана дыра на миллиарды
В чём подвох Строка
Контракт-грабитель:
Один
Почему это не экзотика. Так в 2016 украли The DAO - 3.6 млн ETH, и ради отката той кражи Ethereum раскололся на две сети (словарь №18, форк). Ровно эта щель между «отдал» и «записал» до сих пор в топе аудиторских находок
Задание №16: прочитай
Код грабителя и дырявой Копилки в репо, тег post-16. Разворачивай на локальной ноде и грабь на здоровье: свои же деньги
Райт · blockwright.eth
github.com/blockwright/kopilka · tag post-16
Добавим человечную фичу Копилка копит на цель. Логично разрешить передумать: пока цель не достигнута -забери свой вклад обратно. Для этого контракт запоминает, кто сколько внёс, и отдаёт по запросу:
mapping(address => uint256) public deposits;
function refund() external {
uint256 amount = deposits[msg.sender];
if (amount == 0) revert NothingToRefund();
(bool ok, ) = msg.sender.call{value: amount}(""); // 1. отдали деньги
require(ok, "refund failed");
deposits[msg.sender] = 0; // 2. обнулили вклад
}
Выглядит безобидно. Читается сверху вниз, как обычный JS. И здесь спрятана дыра на миллиарды
В чём подвох Строка
msg.sender.call{value: amount} - это не просто перевод. Если msg.sender — контракт, то в момент получения денег у него вызывается код. А наш баланс обнуляется только на следующей строке. Улавливаешь? Между «отдали» и «записали, что отдали» есть щель - и в неё можно засунуть рукуКонтракт-грабитель:
contract Robber {
Kopilka public target;
uint256 bait;
function attack() external payable {
bait = msg.value;
target.deposit{value: bait}(); // вносим приманку
target.refund(); // и сразу забираем — начинается рекурсия
}
// сюда прилетают деньги от refund — и мы бьём снова
receive() external payable {
if (address(target).balance >= bait) {
target.refund(); // наш вклад ещё не обнулён — Копилка отдаёт опять
}
}
}Один
attack() - и рекурсия выкачивает Копилку досуха: каждый повторный refund() видит наш вклад всё ещё ненулевым (обнуление-то не наступило, мы не вышли из первого вызова) и покорно отдаёт чужие деньги, пока баланс не кончится. Внесли 0.01 - вынесли всёПочему это не экзотика. Так в 2016 украли The DAO - 3.6 млн ETH, и ради отката той кражи Ethereum раскололся на две сети (словарь №18, форк). Ровно эта щель между «отдал» и «записал» до сих пор в топе аудиторских находок
Задание №16: прочитай
refund() ещё раз и, не подглядывая в следующий пост, ответь себе: какую одну строчку надо переставить, чтобы дыра закрылась? Это буквально вопрос на миллиард - в четверг проверимКод грабителя и дырявой Копилки в репо, тег post-16. Разворачивай на локальной ноде и грабь на здоровье: свои же деньги
Райт · blockwright.eth
github.com/blockwright/kopilka · tag post-16
Во время работы над своими проектами всё время всплывает желание сделать что-то ещё. Добавить фичу, которую никто не просил. Переписать пакет с нуля, потому что чужой закрывает мой случай на восемьдесят процентов, а оставшиеся двадцать делает не так.
Каждая такая мысль по отдельности выглядит разумной. Она правда сделает проект лучше. Проблема в том, что их много, я один, и они не кончаются: закрываешь одну - приходят две.
Интересно, где проходит эта граница. Между всеми хотелками и реальными возможностями одного человека. Как понять, что вот здесь уже хватит, пора остановиться и заниматься только живыми проектами?
#мысли_вслух
Каждая такая мысль по отдельности выглядит разумной. Она правда сделает проект лучше. Проблема в том, что их много, я один, и они не кончаются: закрываешь одну - приходят две.
Интересно, где проходит эта граница. Между всеми хотелками и реальными возможностями одного человека. Как понять, что вот здесь уже хватит, пора остановиться и заниматься только живыми проектами?
#мысли_вслух
#словарь_райта · №21 - Индексер
Попробуй спросить у контракта: «покажи все депозиты Копилки за август». Не выйдет - у блокчейна нет SELECT WHERE. Контракт отвечает только про текущее состояние, а история лежит в логах событий, размазанных по миллионам блоков
Индексер (индексатор) - это сервис, который слушает события контрактов, складывает их в обычную базу данных и даёт наружу нормальный API с фильтрами и сортировками. Блокчейн остаётся источником истины, индексатор быстрой read-моделью поверх него
Узнаёшь паттерн? Это CQRS: пишем в одно хранилище, читаем из другого. Блокчейн - write-модель, индексер - read-модель, события - конвейер между ними. Всё, что мы собирали в серии, сложилось в знакомую архитектуру
Зачем тебе это слово: без индексатора не живёт ни один серьёзный dApp - история операций, лидерборды, аналитика приходят оттуда. Крупнейший децентрализованный индексатор - The Graph
Райт · blockwright.eth
Попробуй спросить у контракта: «покажи все депозиты Копилки за август». Не выйдет - у блокчейна нет SELECT WHERE. Контракт отвечает только про текущее состояние, а история лежит в логах событий, размазанных по миллионам блоков
Индексер (индексатор) - это сервис, который слушает события контрактов, складывает их в обычную базу данных и даёт наружу нормальный API с фильтрами и сортировками. Блокчейн остаётся источником истины, индексатор быстрой read-моделью поверх него
Узнаёшь паттерн? Это CQRS: пишем в одно хранилище, читаем из другого. Блокчейн - write-модель, индексер - read-модель, события - конвейер между ними. Всё, что мы собирали в серии, сложилось в знакомую архитектуру
Зачем тебе это слово: без индексатора не живёт ни один серьёзный dApp - история операций, лидерборды, аналитика приходят оттуда. Крупнейший децентрализованный индексатор - The Graph
Райт · blockwright.eth
Делал поддержку Solana в кошельке. Фича была запланирована на неделю - сделал за два дня. И дело не в том, что я стал быстрее печатать: архитектура была продумана заранее, и новая сеть легла в уже готовые места, а не поперёк них
Освободившиеся дни уйдут не на следующую фичу. На менеджмент проекта, на соцсети, на то, до чего обычно не доходят руки
Вот что, по-моему, важно в эпоху ИИ. Писать код стало дёшево, и в сторы хлынул поток навайбленных приложений. Только 99 процентов из них - полная херня, которая не выживет, а действительно стоящих продуктов стало даже меньше, чем раньше
Отличает их ровно то, чего ИИ за тебя не делает: архитектура и системность. Когда они есть, он превращает неделю в два дня. Когда их нет - просто быстрее производит мусор
#мысли_вслух
Освободившиеся дни уйдут не на следующую фичу. На менеджмент проекта, на соцсети, на то, до чего обычно не доходят руки
Вот что, по-моему, важно в эпоху ИИ. Писать код стало дёшево, и в сторы хлынул поток навайбленных приложений. Только 99 процентов из них - полная херня, которая не выживет, а действительно стоящих продуктов стало даже меньше, чем раньше
Отличает их ровно то, чего ИИ за тебя не делает: архитектура и системность. Когда они есть, он превращает неделю в два дня. Когда их нет - просто быстрее производит мусор
#мысли_вслух
❤1👍1
В прошлый раз мы выкачали Копилку досуха через
Это всё - две строчки поменялись местами и дыры больше нет. Когда грабитель повторно войдёт в
У этого порядка есть имя Checks-Effects-Interactions - паттерн, который в Solidity надо впечатать в подкорку:
- Checks - сначала все проверки. Кто вызвал, хватает ли, есть ли что отдавать?
- Effects - потом меняем своё состояние. Обнуляем, списываем, помечаем
- Interactions - и только в самом конце трогаем внешний мир, делаем переводы, вызовы чужих контрактов и далее
Причина ровно та, что мы видели вживую, в прошлом посте, любой внешний вызов может вернуться к тебе рекурсией, пока ты не закончил. Поэтому к моменту, когда ты отдаёшь управление наружу, твоё состояние уже должно быть честным и финальным. Отдавай деньги последним действием, когда врать о балансе уже нечем
Аналогия для web2. Это как транзакция в базе, где ты сначала фиксируешь списание денег, а потом дёргаешь внешний платёжный API, а не наоборот. Дёрнешь API раньше коммита - и злобный ретрай может бклиниться между внешним api и твоим INSERT в базу, как итог двойное списание средств. Тот же баг, но в блокчейне за такое платят миллионами потерь, а не саппортом
Докажем, что закрыли. Хороший фикс тот, у которого есть тест. Берём того же грабителя из поста 16 и убеждаемся, что теперь он падает
Тест из поста 16 (атака удаётся) теперь инвертирован в «атака откатывается» и это красиво: один и тот же грабитель до фикса выносит кассу, после фикса разбивает нос о
Задание №17: возьми свою Копилку из поста 16, переставь две строки и прогони тест с грабителем. Красный стал зелёным? Ты только что закрыл дыру, из-за которой Ethereum когда-то раскололся надвое
Райт · blockwright.eth
github.com/blockwright/kopilka · tag post-17
refund(). Задание было - найти строчку, которую надо переставить и ответ смехотворно прост:function refund() external {
uint256 amount = deposits[msg.sender];
if (amount == 0) revert NothingToRefund();
deposits[msg.sender] = 0; // сначала записали
(bool ok, ) = msg.sender.call{value: amount}(""); // потом отдали
require(ok, "refund failed");
}Это всё - две строчки поменялись местами и дыры больше нет. Когда грабитель повторно войдёт в
refund(), его вклад уже обнулён: amount == 0, revert, рекурсия захлёбывается на первом же витке.У этого порядка есть имя Checks-Effects-Interactions - паттерн, который в Solidity надо впечатать в подкорку:
- Checks - сначала все проверки. Кто вызвал, хватает ли, есть ли что отдавать?
- Effects - потом меняем своё состояние. Обнуляем, списываем, помечаем
- Interactions - и только в самом конце трогаем внешний мир, делаем переводы, вызовы чужих контрактов и далее
Причина ровно та, что мы видели вживую, в прошлом посте, любой внешний вызов может вернуться к тебе рекурсией, пока ты не закончил. Поэтому к моменту, когда ты отдаёшь управление наружу, твоё состояние уже должно быть честным и финальным. Отдавай деньги последним действием, когда врать о балансе уже нечем
Аналогия для web2. Это как транзакция в базе, где ты сначала фиксируешь списание денег, а потом дёргаешь внешний платёжный API, а не наоборот. Дёрнешь API раньше коммита - и злобный ретрай может бклиниться между внешним api и твоим INSERT в базу, как итог двойное списание средств. Тот же баг, но в блокчейне за такое платят миллионами потерь, а не саппортом
Докажем, что закрыли. Хороший фикс тот, у которого есть тест. Берём того же грабителя из поста 16 и убеждаемся, что теперь он падает
it("после фикса грабитель не может вынести чужое", async function () {
const kopilka = await deploy();
// жертва вносит 1 ETH
await kopilka.write.deposit({ value: parseEther("1"), account: victim.account });
// грабитель вносит 0.01 и пытается дренажить
const robber = await viem.deployContract("Robber", [kopilka.address]);
await viem.assertions.revert(
robber.write.attack({ value: parseEther("0.01") }),
);
// 1 ETH жертвы на месте
const [current] = await kopilka.read.progress();
assert.equal(current, parseEther("1.01"));
});Тест из поста 16 (атака удаётся) теперь инвертирован в «атака откатывается» и это красиво: один и тот же грабитель до фикса выносит кассу, после фикса разбивает нос о
revert. Регрессия на уязвимость лучший вид тестаЗадание №17: возьми свою Копилку из поста 16, переставь две строки и прогони тест с грабителем. Красный стал зелёным? Ты только что закрыл дыру, из-за которой Ethereum когда-то раскололся надвое
Райт · blockwright.eth
github.com/blockwright/kopilka · tag post-17
#словарь_райта · №22 - Атомарность
Буква A из ACID, которую ты знаешь по транзакциям в базах: операция либо выполняется целиком, либо не выполняется вообще. Половины не бывает
В web3 это не свойство базы, а закон исполнения любого вызова. Транзакция к контракту всегда одна атомарная единица. Сработал require где-то в середине, значит откатывается всё, до последнего изменения, как будто вызова и не было вовсе. Помнишь «вышибалу» из Копилки? Он не просто отказывает, он стирает все следы попытки
Атомарность в web3 сквозная, что значит, один вызов может пройти через пять контрактов - обменять токены, взять кредит, погасить его - и вся цепочка либо удастся целиком, либо целиком испарится. На этом стоят флеш-кредиты: миллионы без залога на одну транзакцию, потому что «не вернул» означает «займа никогда не было»
Зачем тебе это слово: атомарность - твой главный страховочный трос в контрактах, если ошибешся - транзакция откатилась, деньги целы, заплатить придется только за газ, потому что работу-то узлы выполнили
Райт · blockwright.eth
Буква A из ACID, которую ты знаешь по транзакциям в базах: операция либо выполняется целиком, либо не выполняется вообще. Половины не бывает
В web3 это не свойство базы, а закон исполнения любого вызова. Транзакция к контракту всегда одна атомарная единица. Сработал require где-то в середине, значит откатывается всё, до последнего изменения, как будто вызова и не было вовсе. Помнишь «вышибалу» из Копилки? Он не просто отказывает, он стирает все следы попытки
Атомарность в web3 сквозная, что значит, один вызов может пройти через пять контрактов - обменять токены, взять кредит, погасить его - и вся цепочка либо удастся целиком, либо целиком испарится. На этом стоят флеш-кредиты: миллионы без залога на одну транзакцию, потому что «не вернул» означает «займа никогда не было»
Зачем тебе это слово: атомарность - твой главный страховочный трос в контрактах, если ошибешся - транзакция откатилась, деньги целы, заплатить придется только за газ, потому что работу-то узлы выполнили
Райт · blockwright.eth
❤1
This media is not supported in your browser
VIEW IN TELEGRAM
Академия. Сайт релизнут. У обучения появились модули и дерево курса - видно, куда идёшь и чем всё кончается. У кошелька и у академии на сайте
теперь свои страницы
Кошелёк. Собрал Solana. Важные экраны закрыл от скриншотов. Балансы обновляются сами. Релиз завтра - сборка пока закрытая, стоит только у меня. TestFlight - 5 октября.
Всё, что вышло за неделю, - на blockwright.dev
#строй_от_райт
теперь свои страницы
Кошелёк. Собрал Solana. Важные экраны закрыл от скриншотов. Балансы обновляются сами. Релиз завтра - сборка пока закрытая, стоит только у меня. TestFlight - 5 октября.
Всё, что вышло за неделю, - на blockwright.dev
#строй_от_райт
#словарь_райта · №23 - Событие
В web2 «сообщить наружу» - это webhook или EventEmitter. Контракт так не умеет. Он живёт в детерминированной клетке (словарь №8) и не может дёрнуть твой сервер
Вместо этого он кричит в журнал:
Снаружи на журнал подписываются все, кому интересно и твой фронтенд ловит событие и обновляет страницу, индексатор (словарь №21) складывает миллионы таких криков в удобную базу
Зачем тебе это слово: события - это API контракта наружу. Записать состояние может транзакция, а вот рассказать миру, что случилось, только событие
Райт · blockwright.eth
В web2 «сообщить наружу» - это webhook или EventEmitter. Контракт так не умеет. Он живёт в детерминированной клетке (словарь №8) и не может дёрнуть твой сервер
Вместо этого он кричит в журнал:
emit Deposited(кто, сколько). Крик записывается в блок как лог, а это дешёвая строчка истории, которую сам контракт никогда не прочитает. Она не для него, а для внешнего мираСнаружи на журнал подписываются все, кому интересно и твой фронтенд ловит событие и обновляет страницу, индексатор (словарь №21) складывает миллионы таких криков в удобную базу
Зачем тебе это слово: события - это API контракта наружу. Записать состояние может транзакция, а вот рассказать миру, что случилось, только событие
Райт · blockwright.eth
❤1
В прошлом посте мы закрыли дыру перестановкой строк и это работает. Но есть одна проблема, все держится на том, что ты каждый раз вспомнишь про правильный порядок. А функций в контракте десять, писал их в разное время. Память самая плохая защита для денег
Замок вместо памяти. Есть приём, знакомый любому, кто писал многопоточку - mutex. Ставим флаг «занято» на входе в функцию и снимаем на выходе, в итоге повторный вход, пока мы внутри, просто отбивается
Грабитель из поста 16 входит первый раз - ставит флаг - пытается войти рекурсией второй раз -
А теперь — главный урок серии про безопасность. Не пиши этот модификатор сам. У OpenZeppelin есть
Почему чужой код здесь лучше твоего. В обычной разработке «написал сам» часто плюс, потому что понимаешь каждую строку. В безопасности контрактов - это минус. Твой свежий mutex видели ты и ревьюер, а вот
Тот же принцип, кстати, я держу в кошельке, который пишу: для мнемоники и подписи только аудированные библиотеки семейства noble/scure, ни одной своей строчки криптографии. Изобретать своё в этих местах - не смелость, а самонадеянность
Когда что. Checks-Effects-Interactions (пост 17) - базовая гигиена, применяй всегда и бесплатно.
Задание №18: поставь @openzeppelin/contracts, унаследуй Копилку от
В следующем посте выводим Копилку из песочницы в настоящую сеть - деплой в Sepolia, где всё по-взрослому
Райт · blockwright.eth
github.com/blockwright/kopilka · tag post-18
Замок вместо памяти. Есть приём, знакомый любому, кто писал многопоточку - mutex. Ставим флаг «занято» на входе в функцию и снимаем на выходе, в итоге повторный вход, пока мы внутри, просто отбивается
bool private locked;
modifier noReentry() {
require(!locked, "reentrant");
locked = true;
_; // тело функции выполняется здесь
locked = false;
}
function refund() external noReentry {
// ... хоть в каком порядке — второй вход не пройдёт
}
Грабитель из поста 16 входит первый раз - ставит флаг - пытается войти рекурсией второй раз -
require(!locked) отбивает. Рекурсия мертва в зародыше, и уже неважно, в каком порядке внутри строки. Это тот же паттерн, что lock в многопоточном коде, когда в критическую секцию, пускают исключительно по одномуА теперь — главный урок серии про безопасность. Не пиши этот модификатор сам. У OpenZeppelin есть
ReentrancyGuard - тот же mutex, но написанный, вычитанный и проверенный тысячами проектов и десятками аудитовimport "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract Kopilka is ReentrancyGuard {
function refund() external nonReentrant {
// защита из коробки
}
}
Почему чужой код здесь лучше твоего. В обычной разработке «написал сам» часто плюс, потому что понимаешь каждую строку. В безопасности контрактов - это минус. Твой свежий mutex видели ты и ревьюер, а вот
ReentrancyGuard видели тысячи глаз, его ломали на сотнях аудитов и не сломали. В коде, который стережёт деньги, возраст и количество атак, которые он пережил - это не скука, это броня. Меньше своего кода на критическом пути - меньше поверхность атакиТот же принцип, кстати, я держу в кошельке, который пишу: для мнемоники и подписи только аудированные библиотеки семейства noble/scure, ни одной своей строчки криптографии. Изобретать своё в этих местах - не смелость, а самонадеянность
Когда что. Checks-Effects-Interactions (пост 17) - базовая гигиена, применяй всегда и бесплатно.
nonReentrant - ремень безопасности сверху, особенно там, где внешних вызовов несколько или логика ветвится. Одно не отменяет другое: сначала правильный порядок, потом замок как страховкаЗадание №18: поставь @openzeppelin/contracts, унаследуй Копилку от
ReentrancyGuard, повесь nonReentrant на refund - и прогони тест с грабителем из поста 16. Две независимые защиты на одной функции: пояс и подтяжкиВ следующем посте выводим Копилку из песочницы в настоящую сеть - деплой в Sepolia, где всё по-взрослому
Райт · blockwright.eth
github.com/blockwright/kopilka · tag post-18
❤2
Девятнадцать постов Копилка жила на локальной ноде, которая живёт в памяти и умирает с закрытием терминала. Сегодня она попадает в настоящую сеть - Sepolia, тестнет Ethereum. Те же ноды, тот же консенсус, те же блоки раз в 12 секунд, только эфир бесплатный
Помнишь env из поста 5? Вот ради чего. Пять постов назад мы прятали RPC-адрес и приватный ключ в переменные и не коммитили их. Тогда это выглядело перестраховкой. Сегодня они, наш билет в сеть
Три шага до сети. Наливаем тестовый эфир из фаусета на адрес деплоера, потом Ignition (пост 11) катит контракт, но уже в Sepolia
Разница с локальным деплоем во времени. Локально контракт появлялся мгновенно, здесь ты ждёшь, пока транзакцию подхватит валидатор и подтвердит блок. Секунд десять-двадцать реального ожидания и Ignition печатает адрес, который теперь существует не в твоей памяти, а в общей сети, для всех
Сделаем контракт читаемым для мира. Verify публикует исходник на Etherscan и связывает его с байткодом в сети, чтобы любой мог открыть контракт и прочитать, что он делает
После этого на sepolia.etherscan.io у твоей Копилки появляется вкладка Code с исходником и зелёная галочка. Это и есть web3-этика по умолчанию. Код, который распоряжается деньгами, открыт для чтения. Не «доверься мне», а «прочитай сам»
И фронт оживает по-настоящему. Помнишь, в конфиге из поста 13 мы прописали
Задание №19: задеплой Копилку в Sepolia, дождись адреса, verify на Etherscan и пришли ссылку на свой контракт в комментарии. Это первый твой контракт, который видит весь мир, - момент, который стоит запомнить
Райт · blockwright.eth
github.com/blockwright/kopilka · tag post-19
Помнишь env из поста 5? Вот ради чего. Пять постов назад мы прятали RPC-адрес и приватный ключ в переменные и не коммитили их. Тогда это выглядело перестраховкой. Сегодня они, наш билет в сеть
// hardhat.config.ts — сеть Sepolia
sepolia: {
type: "http",
url: configVariable("SEPOLIA_RPC_URL"),
accounts: [configVariable("SEPOLIA_PRIVATE_KEY")],
}
configVariable не тащит секрет в код - Hardhat подставит его из окружения в момент деплоя. Ключ живёт в .env, .env - в .gitignore, и это не паранойя: приватный ключ в публичном репо боты выметают за секунды. Правило, которое стоит впечатать в подкорку, ключ, которым деплоишь, - от отдельного тестового кошелька. Никогда не основногоТри шага до сети. Наливаем тестовый эфир из фаусета на адрес деплоера, потом Ignition (пост 11) катит контракт, но уже в Sepolia
npx hardhat ignition deploy ignition/modules/Kopilka.ts --network sepoliaРазница с локальным деплоем во времени. Локально контракт появлялся мгновенно, здесь ты ждёшь, пока транзакцию подхватит валидатор и подтвердит блок. Секунд десять-двадцать реального ожидания и Ignition печатает адрес, который теперь существует не в твоей памяти, а в общей сети, для всех
Сделаем контракт читаемым для мира. Verify публикует исходник на Etherscan и связывает его с байткодом в сети, чтобы любой мог открыть контракт и прочитать, что он делает
npx hardhat verify --network sepolia <адрес> "<аргумент goal>"После этого на sepolia.etherscan.io у твоей Копилки появляется вкладка Code с исходником и зелёная галочка. Это и есть web3-этика по умолчанию. Код, который распоряжается деньгами, открыт для чтения. Не «доверься мне», а «прочитай сам»
И фронт оживает по-настоящему. Помнишь, в конфиге из поста 13 мы прописали
sepolia рядом с hardhat? Меняешь в KOPILKA_ADDRESS локальный адрес на свежий из Sepolia, переключаешь MetaMask на Sepolia - и та же страница, кошелёк, живая лента работают против настоящей сети. Кто-то с другого конца света может открыть твою Копилку и внести депозит. Круг, начатый в посте 1, замкнулсяЗадание №19: задеплой Копилку в Sepolia, дождись адреса, verify на Etherscan и пришли ссылку на свой контракт в комментарии. Это первый твой контракт, который видит весь мир, - момент, который стоит запомнить
Райт · blockwright.eth
github.com/blockwright/kopilka · tag post-19
Двадцать постов назад мы договорились: возьмём JS-разработчика и проведём его в web3 не через хайп и «иксы», а через код. Через одну маленькую вещь, собранную до конца. Сегодня она собрана
Посмотри, что у тебя в руках. Смарт-контракт с событиями, кастомными ошибками и защитой от reentrancy. Семь тестов на node:test, включая регрессию на реальную уязвимость. Фронт на React и wagmi: кошелёк, чтение, запись, живая лента. Контракт, задеплоенный в настоящую сеть и открытый на Etherscan. Это не туториал из песочницы - это dApp той же архитектуры, что и большие, просто маленький и честный
Но Копилка была предлогом. Что ты на самом деле унёс за эти двадцать постов - это не мануал по одному контракту, а перенос того, что ты уже умел, на новую территорию:
- event sourcing, который ты знал по Redux, оказался блокчейном
- чистые функции - детерминизмом контракта
- очередь задач - мемпулом
- webhook - событиями
- mutex из многопоточки - защитой от повторного входа
- коммит до внешнего вызова - порядком строк на миллиард
web3 не потребовал выбросить твой опыт. Он потребовал увидеть его под другим углом. Именно это я и обещал в первом посте: не «забудь всё и учи с нуля», а «ты уже наполовину здесь»
Что дальше. Серия закончилась, канал - нет. Дальше я строю настоящее: некастодиальный кошелёк, теми же руками и по тем же правилам - аудированные библиотеки, тесты раньше кода, никакой магии. Копилка была разминкой. Кошелёк - это уже всерьёз. Хочешь смотреть, как это собирается, и разбираться вместе - ты уже подписан, просто останься
Спасибо, что дошёл до двадцатого. Это чего-то стоит - дочитать до конца то, что многие бросают на третьем
Задание №20, последнее и не техническое: напиши в комментарии, что было для тебя самым неожиданным за эти двадцать постов. Не «спасибо, полезно», а конкретно: где ёкнуло «а, так вот оно как». Я прочитаю каждый
Райт · blockwright.eth
github.com/blockwright/kopilka · release v1.0
Посмотри, что у тебя в руках. Смарт-контракт с событиями, кастомными ошибками и защитой от reentrancy. Семь тестов на node:test, включая регрессию на реальную уязвимость. Фронт на React и wagmi: кошелёк, чтение, запись, живая лента. Контракт, задеплоенный в настоящую сеть и открытый на Etherscan. Это не туториал из песочницы - это dApp той же архитектуры, что и большие, просто маленький и честный
Но Копилка была предлогом. Что ты на самом деле унёс за эти двадцать постов - это не мануал по одному контракту, а перенос того, что ты уже умел, на новую территорию:
- event sourcing, который ты знал по Redux, оказался блокчейном
- чистые функции - детерминизмом контракта
- очередь задач - мемпулом
- webhook - событиями
- mutex из многопоточки - защитой от повторного входа
- коммит до внешнего вызова - порядком строк на миллиард
web3 не потребовал выбросить твой опыт. Он потребовал увидеть его под другим углом. Именно это я и обещал в первом посте: не «забудь всё и учи с нуля», а «ты уже наполовину здесь»
Что дальше. Серия закончилась, канал - нет. Дальше я строю настоящее: некастодиальный кошелёк, теми же руками и по тем же правилам - аудированные библиотеки, тесты раньше кода, никакой магии. Копилка была разминкой. Кошелёк - это уже всерьёз. Хочешь смотреть, как это собирается, и разбираться вместе - ты уже подписан, просто останься
Спасибо, что дошёл до двадцатого. Это чего-то стоит - дочитать до конца то, что многие бросают на третьем
Задание №20, последнее и не техническое: напиши в комментарии, что было для тебя самым неожиданным за эти двадцать постов. Не «спасибо, полезно», а конкретно: где ёкнуло «а, так вот оно как». Я прочитаю каждый
Райт · blockwright.eth
github.com/blockwright/kopilka · release v1.0
This media is not supported in your browser
VIEW IN TELEGRAM
Кошелёк есть. v1.4 работает, ключи только у тебя. BTC, ETH, SOL. На том же стеке, что мы крутили всю серию и который будет навсегда с нами - TypeScript и Expo
За время работы над этим проектом уже успел не раз поволноваться. Не потому что что-то не готово или не получается, а потому что система не даёт человеку просто взять и выпустить годный свободный продукт
Всё устроено так, чтобы ты завёл компанию и в итоге работал на крупных игроков. Польза для людей идёт вторым номером - первым стоит прибыль. Я почти уверен: именно это давление и погубило большую часть честных инноваций
За время работы над этим проектом уже успел не раз поволноваться. Не потому что что-то не готово или не получается, а потому что система не даёт человеку просто взять и выпустить годный свободный продукт
Всё устроено так, чтобы ты завёл компанию и в итоге работал на крупных игроков. Польза для людей идёт вторым номером - первым стоит прибыль. Я почти уверен: именно это давление и погубило большую часть честных инноваций
💯1