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 при 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)
}
}
}
—
Как-то так. Два месяца писал и редактировал данную статью, даже успел неточности в коде найти и исправить.
Спасибо, что дочитали.
Есть лишние эфиры на газ – можете через мое мега приложение оставить коммент на мой гитхаб. Это пока платно – 0.0001 эфира, что есть $0.2 (из которых 60% идет обратно в коммьюнити; или не идет), чтобы пользователи привыкали. В дальнейшем планирую функции “оставить коммент”, “лайк/дизлайк” сделать бесплатными. Ну или давайте как у блогеров – 200+ комментов в приложении, 30+ лайков на данный пост, моя карма 8+ — сразу отменю оплату.
Есть работа для меня – можете написать в лс. C# (11 лет), JavaScript (10 лет), SQL (10 лет), NoSQL, Solidity (5 лет), Python (вот начал); high load = 0; в энтерпрайзе с 2009 года.
Автор: tema_rebel


