2021й. Очередная смена компании. Взглянув на рынок, решил перейти из стека дотнет + джс в крипто-разработчики на Solidity. Соответственно, нужен был проект "в портфолио", чтобы заинтересовать эйчаров и технарей. Написал таких несколько:
a. Raffle - лотерея с некоторыми ограничениями (подсказали эйчары). Как тестовое - неплоха: задачка простая, сразу понятная, при этом по коду уже можно оценить архитектурные навыки: тут тебе и прокси, и интерфейсы, и подключение к сторонней системе, и отложенные транзакции (запрос случайного числа), и оптимизация хранилища. На тот момент такого вполне хватало, чтобы зацепить работодателей.
б. On-chain NFT - NFT система, в которой все данные хранятся и, соответственно, "отрисовываются" полностью on-chain, т.е никаких ipfs и сторонних серверов не требуется. Для этого картинка - слоеный svg, который в коде переводишь в base64, как и json аттрибуты, согласно спецификации OpenSea.
Быстро глянул свой код - там дикий оверхед...)
В целом, к NFT делаешь доп контракт на реализацию метода tokenURI, в который шлешь данные токена и получаешь base64 из json аттрибутов, и в этом json в поле image у тебя base64 твоего svg. Можно так же сделать доп. прослойку, в которой ты будешь на каждый слой svg иметь свой контракт, и тогда "мастер-отрисовщик" будет собирать данные из списка этих слоев. В этом случае заменять при необходимости уже не весь svg, а только определенный слой.

в. Мой Пет, он же Shept - chrome(-ium) browser(s) extension, в котором можно оставлять комменты ко всему, что имеет URL (например, к этой статье, или к моему гитхабу, или к инсте твоей бывшей). А так же можно и заработать на этом, если ты - первый, кто написал на странице: с будущих активностей на странице ты будешь иметь процент. Комментарии хранятся как события в блокчейне, поэтому их физически нельзя стереть, удалить, заменить, и даже если остановить смарты и затереть внутренние связи, комментарии все равно можно будет прочитать, зная адрес смарта и интерфейс события, в котором хранятся данные. Это вкратце, подробнее есть в whitepaper, а ниже - параграф про $100k+ и оптимизации Solidity, которые меня зацепили.
Собственно, в 2021м году я с этим проектом (еще в разработке на тот момент), залетаю на техническое интервью, бла-бла, все дела. Попутно и про on-chain-nft говорю, что вот она, настоящая анархия, без внешних зависимостей. Еще и про metamorphic зацепился, что, оказывается, можно деплоить контракт не только в определенный адрес, но еще и прямо на место старого - шах и мат вашим неизменяемым смартам. В итоге получаю должность Director of blockchain development и контракт (не smart, а legal), который мне и принёс в итоге указанные в заголовке деньги. Как оказалось - это был самый прибыльный способ для меня заработать в крипте, а не все эти пи-ту-пи или мои кроссовки Stepn).
Естественно, работа получена - проект заброшен. Вернулся я к нему только в 2025м. И то, если бы не Lovable, я бы не продолжал - застопорился на UI, а эту часть я прям не фанат разгребать. Lovable сделал шаблон, я его расширил на приложение. Сейчас переписал бы фронт с нуля, потому что lovable + tailwind_css = архитектурная дичь.

Оптимизации
Дисклеймер_1. Весь код и все рассуждения, приведенные ниже, действительны изначально для моего проекта, моих структур данных, и моего способа использования данных. На ваших структурах и бизнес логике мои хитрости могут не сработать.
A. Время нужно делить
Это был прям взрыв мозга!
Мне нужно было запилить календарь в Solidity, с возможностью указать для события даты с-до, а так же иметь возможность взять элемент по дате (попаданию в промежуток с-до). Иметь два поля uint32 для структуры, а потом еще как-то искать по ним - вообще не вариант в солидити. Я застрял на этом моменте, поэтому попросил ии накидать мне вариантов. И оно предложило мне делить время (timestamp) на длину интервала! Я был в шоке, насколько это гениально! Ты не ищешь по интервалу, а можешь просто взять по индексу/хешу (текущему id интервала)!
Отметка во времени - slotIndex - это позиция события в безразмерном bytes (bytes myMemory;) (или его аналоге - mapping(uint256 slotsPack => uint256 packedData), подробнее ниже), в который я пишу айди событий. Размер для айди событий я выбрал uint16, и в моем случае этого хватит на 7.5 лет записи нового события в каждый слот.
// find slot index, aka time interval uint256 slotIndex = startTimestamp / eventDuration; // write "eventId" value at "slotIndex" in mapping-mapping // prepare constants uint256 bitsPerItem = 16; // uint16 for eventId uint256 itemsPerPack = 256 / bitsPerItem; // = 16 uint256 bitmask = (1 << itemsPerPack) - 1; // = ffff // find position in mapping-mapping uint256 slotsPack = slotIndex / itemsPerPack; uint256 indexInPack = slotIndex % itemsPerPack; // read current packed value (16 events in some slot) uint256 packedValue = packed$.[slotsPack]; // insert the new eventId packedValue |= (uint256(eventId) << (indexInPack * bitsPerItem)); // store packed$.[slotsPack] = packedValue;
Недостатки.
Если событие растягивается на несколько интервалов, для каждого интервала надо записать ссылку на событие. Получается, 16 интервалов в 1 слот (т.е что 1 значение записать, что 16 сразу), или же в моей бизнес логике 2 полных дня - ну, норм.
Б. Упаковка массива небольших чисел в mapping-e
Здесь интересный момент. Я как-то говорил на хабре, что mapping(uint256 id => uint8[] dataArray) хранит список полных слотов uint256 вместо упакованных uint8, однако только что затестил в remix ide при sol ver 0.8.34 - это неверное утверждение: все же такой маппинг хранит именно упакованный массив мелких данных. При этом в своем проекте я уже написал либу, которая упаковывает весь мелкий массив в bytes, и вот думал сейчас удалить, но... Сделал еще пару замеров - запись в bytes дороже на 1.3%, чем uint8[], при этом чтение [0..100) из 100 - уже на 21% дешевле. Так что если вы много читаете, чтобы еще и записать, то bytes выглядит перспективнее.
mapping(uint256 id => uint256[]) | mapping(uint256 id => uint8[]) | mapping(uint256 id => bytes) | |
add avg | 27,653 | 11,722 | 11,876 |
read 100 | 255,403 | 81,854 | 64,467 |
Здесь важно: мой текущий способ хранит размерности, на которые слот делится без остатка - 1, 2, 4, 8, 16, 32 байта. Есть схожий (на id => bytes) вариант хранения, который не требует сложную либу для работы:
mapping(uint256 id => mapping(uint256 slotsPackIndex => uint256 packedData)) data
Например, элементы имеют размер 2 байта (16 бит), и нам надо прочитать по индексу 21 (начиная с 0):
- количество в слоте: numInSlot = 32 байта слот / 2 байта размер = 16 штук (это константа)
- берем слот: uint256 packedData = data[id][ 21 / numInSlot ] = data[id][ 21 / 16 ] = data[id][ 1 ]
- берем индекс в слоте: indexInSlot = 21 % numInSlot = 21 % 16 = 5
- читаем данные: return (packedData >> (indexInSlot * numOfBits)) & bitmask (направление сдвига для чтения зависит от направления сдвига при записи).
Для данных, которые имеют другую размерность, можно дописать либу, чтобы она смотрела на выход за размер слота, но я бы делал через упакованный маппинг (mapping(id => mapping(packIndex => packedData)). Вот сравнительная таблица потребления газа:
m(id => uint32[]) | m(id => bytes) (1,2,4,8,16,32) | m(id => bytes) (other sizes) | m(id => m(slotsPackInd => packed)) | |
push 128 items, avg | 34,027 | 34,186 | 34,250 | 34,534 |
read 20 from page 3 | 50,228 | 40,954 | 42,234 | 44,576 |
Код смарта
Скрытый текст
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract GasPacked { uint256 constant TOPIC_ID = 123; uint256 counter = 1; mapping(uint256 => uint16[]) public smallItems; mapping(uint256 => uint256[]) public hugeItems; mapping(uint256 => bytes) public packedItems; // ── Write ──────────────────────────────────────────────────────── function empty() public { counter++; } function pushSmall (uint16 _val) public { smallItems[TOPIC_ID].push(_val); empty(); } function pushHuge (uint256 _val) public { hugeItems [TOPIC_ID].push(_val); empty(); } function pushPacked(uint16 _val) public { LibPackedStorageAligned.push(packedItems[TOPIC_ID], _val, 2); empty(); } // ── Gas benchmark ──────────────────────────────────────────────── function benchSmall(uint256 _count) public returns (uint256 sum) { uint16[] storage arr = smallItems[TOPIC_ID]; for (uint256 i; i < _count; i++) sum += arr[i]; empty(); } function benchHuge(uint256 _count) public returns (uint256 sum) { uint256[] storage arr = hugeItems[TOPIC_ID]; for (uint256 i; i < _count; i++) sum += arr[i]; empty(); } function benchPacked(uint256 _count) public returns (uint256 sum) { (uint256[] memory items, ) = LibPackedStorageAligned.readPage(packedItems[TOPIC_ID], 0, _count, 2); for (uint256 i; i < _count; i++) sum += items[i]; empty(); } // read raw slot from 'smallItems' function readRawSmallSlot(uint256 _slotIndex) public view returns (uint256 _slotData) { bytes32 arraySlot; assembly { mstore(0x00, TOPIC_ID) mstore(0x20, smallItems.slot) arraySlot := keccak256(0x00, 0x40) } bytes32 firstDataSlot = keccak256(abi.encode(arraySlot)); assembly { _slotData := sload(add(firstDataSlot, _slotIndex)) } } } library LibPackedStorageAligned { /// @dev EVM word size in bytes (storage slot, uint256, keccak256 input, memory stride). uint256 internal constant WORD_BYTES = 32; /// @dev Bits per word (WORD_BYTES * 8). Used for full-word mask detection. uint256 internal constant BITS_PER_WORD = 256; error InvalidItemSize(uint256 itemSize); /// @notice Push a new item to the packed storage /// @param data The bytes storage array to append to /// @param item The item value to store (will be masked to itemSize bytes) /// @param itemSize Size of each item in bytes (1-32) function push(bytes storage data, uint256 item, uint256 itemSize) internal { if (itemSize == 0 || itemSize > WORD_BYTES) revert InvalidItemSize(itemSize); assembly ("memory-safe") { // Load base slot let slot := data.slot // Load current length (in bytes) let len := sload(slot) // New length (len + itemSize bytes) let newLen := add(len, itemSize) sstore(slot, newLen) // Storage slot where byte array contents start: keccak256(slot) mstore(0x00, slot) let dataSlot := keccak256(0x00, WORD_BYTES) // We must write at offset = len let slotDiv := div(len, WORD_BYTES) let writeSlot := add(dataSlot, slotDiv) let offset := mod(len, WORD_BYTES) let word := sload(writeSlot) // Shift: item occupies itemSize bytes at offset within WORD_BYTES-byte word let shift := shl(3, sub(sub(WORD_BYTES, offset), itemSize)) // Mask from itemSize: (1 << (itemSize * 8)) - 1 let maskBits := shl(3, itemSize) let itemMask if eq(maskBits, BITS_PER_WORD) { itemMask := not(0) } if iszero(eq(maskBits, BITS_PER_WORD)) { itemMask := sub(shl(maskBits, 1), 1) } // Insert by clearing itemSize bytes then ORing new value // CRITICAL: Mask the item first to ensure only itemSize bytes are used // Otherwise, extra bits in 'item' will corrupt the storage let maskedItem := and(item, itemMask) // Create mask: clear itemSize bytes at position 'shift' let shiftedMask := shl(shift, itemMask) let clearMask := not(shiftedMask) word := and(word, clearMask) word := or(word, shl(shift, maskedItem)) sstore(writeSlot, word) } } /// @notice Get the number of items stored /// @param data The bytes storage array /// @param itemSize Size of each item in bytes (1-32) /// @return count The number of items stored function length(bytes storage data, uint256 itemSize) internal view returns (uint256 count) { if (itemSize == 0 || itemSize > WORD_BYTES) revert InvalidItemSize(itemSize); assembly ("memory-safe") { let slot := data.slot let totalBytes := sload(slot) count := div(totalBytes, itemSize) } } /// @notice Read a page of items from the packed storage /// @param data The bytes storage array /// @param pageNumber Zero-based page number /// @param pageSize Number of items per page /// @param itemSize Size of each item in bytes (1-32) /// @return items Array of items (as uint256, caller should cast as needed) function readPage(bytes storage data, uint256 pageNumber, uint256 pageSize, uint256 itemSize) internal view returns (uint256[] memory items, uint256 totalCount) { if (itemSize == 0 || itemSize > WORD_BYTES) revert InvalidItemSize(itemSize); totalCount = length(data, itemSize); // Special case: if pageSize is 0, return all items if (pageSize == 0) { pageNumber = 0; pageSize = totalCount; } uint256 start = pageNumber * pageSize; if (totalCount == 0 || start >= totalCount) { return (new uint256[](0), totalCount); } uint256 count = pageSize; if (count > totalCount - start) { count = totalCount - start; } items = _readItems(data, start * itemSize, count, itemSize); return (items, totalCount); } /// @notice Read a number of items from the packed storage starting at a given skipItemsCount /// @param data The bytes storage array /// @param skipItemsCount Zero-based item index to start reading from (0 = first item) /// @param count Maximum number of items to read and return /// @param itemSize Size of each item in bytes (1-32) /// @return items Array of items (as uint256, caller should cast as needed). /// Returns fewer items if skipItemsCount + count exceeds total length. function readFromOffset(bytes storage data, uint256 skipItemsCount, uint256 count, uint256 itemSize) internal view returns (uint256[] memory items, uint256 totalCount) { if (itemSize == 0 || itemSize > WORD_BYTES) revert InvalidItemSize(itemSize); totalCount = length(data, itemSize); if (skipItemsCount >= totalCount) { return (new uint256[](0), totalCount); } uint256 itemsToRead = count; if (itemsToRead > totalCount - skipItemsCount) { itemsToRead = totalCount - skipItemsCount; } items = _readItems(data, skipItemsCount * itemSize, itemsToRead, itemSize); return (items, totalCount); } /// @dev Internal helper: reads `count` items starting at `byteOffset` (in bytes). /// Caps count to available items if byteOffset + count would exceed storage. function _readItems(bytes storage data, uint256 byteOffset, uint256 count, uint256 itemSize) private view returns (uint256[] memory items) { assembly ("memory-safe") { let baseSlot := data.slot let totalBytes := sload(baseSlot) // Cap count to available items from byteOffset let availableBytes := sub(totalBytes, byteOffset) let availableItems := div(availableBytes, itemSize) if gt(count, availableItems) { count := availableItems } if iszero(count) { items := mload(0x40) mstore(items, 0) mstore(0x40, add(items, WORD_BYTES)) } if gt(count, 0) { items := mload(0x40) mstore(items, count) let outPtr := add(items, WORD_BYTES) mstore(0x00, baseSlot) let dataSlot := keccak256(0x00, WORD_BYTES) let slotOffset := add(dataSlot, div(byteOffset, WORD_BYTES)) let pos := mod(byteOffset, WORD_BYTES) let maskBits := shl(3, itemSize) let itemMask if eq(maskBits, BITS_PER_WORD) { itemMask := not(0) } if iszero(eq(maskBits, BITS_PER_WORD)) { itemMask := sub(shl(maskBits, 1), 1) } let word := sload(slotOffset) for { let i := 0 } lt(i, count) { i := add(i, 1) } { if iszero(lt(pos, WORD_BYTES)) { slotOffset := add(slotOffset, 1) word := sload(slotOffset) pos := sub(pos, WORD_BYTES) } let shift := shl(3, sub(sub(WORD_BYTES, pos), itemSize)) let extracted := and(shr(shift, word), itemMask) mstore(outPtr, extracted) outPtr := add(outPtr, WORD_BYTES) pos := add(pos, itemSize) } mstore(0x40, outPtr) } } } }
Код части тестов
Скрытый текст
/* eslint-disable no-undef */ // Right click on the script name and hit "Run" to execute const { expect } = require("chai"); const { ethers } = require("hardhat"); describe("GasPacked", function () { const deployGp = async () => { const GasPacked = await ethers.getContractFactory("GasPacked"); const ctr = await GasPacked.deploy(); await ctr.deployed(); return ctr; } const getExtraGas = async (ctr) => { const emTx = await ctr.empty() const emRec = await emTx.wait() return BigInt(emRec.gasUsed) } it('run 100 small', async () => { const gp = await deployGp() const extra = await getExtraGas(gp) let gas = 0n let gases = [] for (let i = 1; i <= 100; i++) { const tx = await gp.pushSmall(i) const receipe = await tx.wait() const gasUsed = BigInt(receipe.gasUsed) - extra gas += gasUsed gases.push(gasUsed) } const avg = gas/100n console.log('AVG: ', avg) console.log('_array_', gases) const readGasTx = await gp.benchSmall(100) const readGasRec = await readGasTx.wait() console.log('read gas: ', BigInt(readGasRec.gasUsed) - extra) }) it('run 100 huge', async () => { const gp = await deployGp() const extra = await getExtraGas(gp) let gas = 0n let gases = [] for (let i = 1; i <= 100; i++) { const tx = await gp.pushHuge(i) const receipe = await tx.wait() const gasUsed = BigInt(receipe.gasUsed) - extra gas += gasUsed gases.push(gasUsed) } const avg = gas/100n console.log('AVG: ', avg) console.log('_array_', gases) const readGasTx = await gp.benchHuge(100) const readGasRec = await readGasTx.wait() console.log('read gas: ', BigInt(readGasRec.gasUsed) - extra) }) it('run 100 packed', async () => { const gp = await deployGp() const extra = await getExtraGas(gp) let gas = 0n let gases = [] for (let i = 1; i <= 100; i++) { const tx = await gp.pushPacked(i) const receipe = await tx.wait() const gasUsed = BigInt(receipe.gasUsed) - extra gas += gasUsed gases.push(gasUsed) } const avg = gas/100n console.log('AVG: ', avg) console.log('_array_', gases) const readGasTx = await gp.benchPacked(100) const readGasRec = await readGasTx.wait() console.log('read gas: ', BigInt(readGasRec.gasUsed) - extra) }) });
В. Proxy erc-1967 vs Diamond erc-2535
Для скорости написания и возможности внесения правок, изначально все построил на proxy-1967. Однако, когда система начала разрастаться, я заметил, что в рамках одного запроса часто делаю много одинаковых проверок ролей и доступов, поскольку прыгаю из контракта в контракт. Тогда решил перейти на diamond-2535 (я использую 3ю версию; обновил ее под новые версии solidity и hardhat здесь). Пересмотрев еще раз архитектуру приложения, пару контрактов оставил под proxy-1967, основную часть перевел на diamond-2535, а еще пару оставил вообще без прокси (бекон и accessControl).
Как итог - снижение потребления газа на основные (тяжелые) функции на 20%, а на некоторых мелких участках замеров - до 80%.
Г. Двусвязный список с якорями для длинных списков
Дисклеймер_2: я изучал алгоритмы и структуры данных 15+ лет назад в универе. Однако ни разу за 15 лет работы это не потребовалось. Только в прошлом году, возможно, для некоторых фич моего solidity кода эти алгоритмы могли бы пригодиться. До сих пор считаю, что ради 1 раза в 15 лет хватит посмотреть ютуба разок или чисто из головы что-то придумать. Поэтому если ниже я описал очередной велосипед - ну, сорян, можете пролистать.
Пример, как я "придумал" систему хранения данных
В начале моей solidity карьеры я придумал, что для сохранения storage layout при замене контрактов можно хранить данные в отдельном месте как key-value, а через какое-то время оказалось, что это называется eternal storage и давно существует.
Так вот. Были у меня списки комментариев к страницам. Список id комментов привязывается к pageId как упакованный bytes, текстовые данные - в событии (event CommentCreated), данные для некоторых фильтраций - пакуются в отдельный слот. Изначально не предполагалась никакая сортировка и удаление, поэтому просто пихал comment id в bytes один за другим.
mapping(uint256 pageId => bytes comments) pageComments;data: id1 = [ bits 0..15 ], id2 = [ bits 16..31 ], id3 = [ bits 32..47 ]...
Собственно, появилась необходимость добавить в этот массив возможность переставлять любой элемент на первое место - сам массив id был удален, на его место пришел двусвязный список (скорее всего, подошел бы и односвязный, не могу вспомнить, есть ли у меня чтение в обратную сторону), который хранится уже в структуре самого коммента (той части данных, которая у меня лежит на смарте) - для меня потребовалось записать туда 64 лишних бита (prev, next); вместилось. Но, допустим, у нас много объектов и надо прочитать 10 последних из 1000. Что делать?
Якоря. Допустим, читаю в основном 1 страницу за раз, допускаю прочтение 1 "лишней" страницы. Тогда каждый первый элемент четной страницы, начиная со 2й, записываю в доп. bytes - своеобразные якоря. Для первых двух страниц (стр 0 и стр 1) якоря уже есть в "родительском" элементе - head для двусвязного списка. Поэтому сохраняем со 2й страницы.
mapping(uint256 pageId => bytes anchors) pageAnchors;data: id51 = [ bits 0..15 ], id101 = [ bits 16..31 ], id151 = [ bits 32..47 ], id201 = [ bits 32..47 ]...
Тогда, имея 50 элементов на 1 якорь, переставляя последний на первое место, исключая для статистики 50 первых элементов (управляются через head), мы пишем всего 1 слот на 400 элементов (не считая обновления head/tail, и ссылок нового последнего на 0, нового первого на старый первый). В итоге мы запишем 3 слота (head+tail, first item, last item) в любом случае, плюс 1 слот на каждые 400 элементов - мне кажется норм.
Как прочитать страницу, например, 25 элементов с индекса 125. Берем 125 за отправной индекс, ищем якорь. Поскольку якорь обрабатывает 50 элементов, то начнем читать с индекса 100, и сделаем 25 "пустых" прыжков до индекса 125, а там уже прочтем страницу.
Как найти элемент, чтобы его переставить? Например, тот же 125й. Для этого надо знать его индекс - передаем индекс с фронта. Но ведь порядок может измениться! Да, поэтому этот индекс считаем базовым и пробуем прочитать какое-то количество элементов, начиная с него. Если нашли нужный - отлично, не нашли - revert и не тратим газ. Операция (переместить наверх) платная, так что читать много не придется (дорого), а если и придется, то заработанных эфиров хватит, чтобы это все переписать и оптимизировать.
Д. Переиспользование ID
В рамках одного запроса мне иногда надо сохранить несколько сущностей в раздельных смартах и/или подсистемах. Допустим, в рамках одной транзакции я создаю новый NFT в одном смарте, и пользователя создаю в другом. Тогда у меня есть отдельно nftId и userId, и, соответственно, по счетчику для каждого. При этом, nftId создается в любом случае.
Я позитивен, поэтому для nftId я держу 32 бита, или 4 миллиарда значений. Вопрос: будет ли у меня больше 4 миллиардов пользователей? Ну, скорее всего, нет. Поэтому если пользователь у меня не сохранен, то вместо отдельного счетчика пользователей я беру текущее значение нового созданного nft. Да, айди пользователей будут идти с пробелами, но, слушайте, если кто-то купит у меня 4 миллиарда nft, я буду богаче Илона Маска, и уж точно смогу нанять более умных людей, чтобы решили вопрос) А пока, если я могу переиспользовать чужой айди, то я буду так делать.
Е. Пауза
Пожалуй, самое стандартное и очевидное. Во-первых, дешевле оперировать, если хранится не булевое true/false, а 1 или 2. Во-вторых, раз уж мы имеем целый слот под это дело (или минимум для uint - 8 бит), то можно уже хранить не 1 или 2, а битовые маски на те или иные части логики, которые можно отдельно ставить на паузу, и сравнивать уже return storageValue & 0x100 > 0.
Ж. Дополнительно: Длина строк
Это не оптимизация, но особенность работы с UTF-8 строками, которая для меня оказалась неожиданна: каждый символ может занимать от 1 до 4 байт, при этом в первых битах хранится его длина. При этом в строке могут быть символы разной размерности. Поэтому чтобы посчитать длину строки, недостаточно сконвертировать ее в bytes и затем взять length, надо пройти по каждому символу и узнать его размер.
В примере ниже мы берем первый (очередной) байт let c := byte(0, calldataload(i)) , и затем смотрим, что в первых битах (это три шага shr ). Если находим 0, 6, Е, 1Е, то это соответственно длина 1, 2, 3, 4, иначе - косяк. Двигаем i по строке на нужное количество байт и берем очередной байт и смотрим в нем очередную длину символа.
function getLengthFromCalldata(string calldata _input) internal pure returns (uint256 _len) { assembly { let i := _input.offset let end := add(i, _input.length) for {} lt(i, end) {} { let c := byte(0, calldataload(i)) switch shr(7, c) case 0 { i := add(i, 1) } default { switch shr(5, c) case 0x6 { i := add(i, 2) } default { switch shr(4, c) case 0xE { i := add(i, 3) } default { switch shr(3, c) case 0x1E { i := add(i, 4) } default { revert(0, 0) } } } } _len := add(_len, 1) } } }
---
Как-то так. Два месяца писал и редактировал данную статью, даже успел неточности в коде найти и исправить.
Спасибо, что дочитали.
Есть лишние эфиры на газ - можете через мой "платный твиттер" Shept оставить коммент на мой гитхаб. Стоит сейчас 0.0001 эфира, что есть $0.2 (из которых 60% идет обратно в коммьюнити; или не идет), чтобы пользователи привыкали. В дальнейшем планирую функции "оставить коммент", "лайк/дизлайк" сделать бесплатными. Ну или давайте как у блогеров - 200+ комментов в приложении, 30+ лайков на данный пост, моя карма 8+ -- сразу отменю оплату.
Есть работа для меня - можете написать в лс. C# (11 лет), JavaScript (10 лет), SQL (10 лет), NoSQL, Solidity (5 лет), Python (вот начал); high load = 0; в энтерпрайзе с 2009 года.
