- BrainTools - https://www.braintools.ru -

Архитектурные изыски моего solidity pet-проекта, который принес $100k+ до релиза

2021й. Очередная смена компании. Взглянув на рынок, решил перейти из стека дотнет + джс в крипто-разработчики на Solidity [1]. Соответственно, нужен был проект “в портфолио”, чтобы заинтересовать эйчаров и технарей. Написал таких несколько:

a. Raffle [2] – лотерея с некоторыми ограничениями (подсказали эйчары). Как тестовое – неплоха: задачка простая, сразу понятная, при этом по коду уже можно оценить архитектурные навыки: тут тебе и прокси, и интерфейсы, и подключение к сторонней системе, и отложенные транзакции (запрос случайного числа), и оптимизация хранилища. На тот момент такого вполне хватало, чтобы зацепить работодателей.

б. On-chain NFT [3] – NFT система, в которой все данные хранятся и, соответственно, “отрисовываются” полностью on-chain, т.е никаких ipfs и сторонних серверов не требуется. Для этого картинка – слоеный svg, который в коде переводишь в base64, как и json аттрибуты, согласно спецификации OpenSea [4].

Быстро глянул свой код – там дикий оверхед…)

В целом, к NFT делаешь доп контракт на реализацию метода tokenURI [5], в который шлешь данные токена и получаешь base64 из json аттрибутов, и в этом json в поле image у тебя base64 твоего svg. Можно так же сделать доп. прослойку, в которой ты будешь на каждый слой svg иметь свой контракт, и тогда “мастер-отрисовщик” будет собирать данные из списка этих слоев. В этом случае заменять при необходимости уже не весь svg, а только определенный слой.

примерно так

примерно так

в. Мой Пет, он же Shept [6] – chrome(-ium) browser(s) extension, в котором можно оставлять комменты ко всему, что имеет URL (например, к этой статье, или к моему гитхабу, или к инсте твоей бывшей). А так же можно и заработать на этом, если ты – первый, кто написал на странице: с будущих активностей на странице ты будешь иметь процент. Комментарии физически нельзя стереть, удалить, заменить, и даже если остановить смарты и затереть внутренние связи, комментарии все равно можно будет прочитать, зная адрес смарта и интерфейс события, в котором хранятся данные. Это вкратце, подробнее есть в whitepaper [7], а ниже – параграф про $100k+ и оптимизации Solidity, которые меня зацепили.

Собственно, в 2021м году я с этим проектом (еще в разработке на тот момент), залетаю на техническое интервью, бла-бла, все дела. Попутно и про on-chain-nft говорю, что вот она, настоящая анархия, без внешних зависимостей. Еще и про metamorphic [8] зацепился, что, оказывается, можно деплоить контракт не только в определенный адрес, но еще и прямо на место старого – шах и мат вашим неизменяемым смартам. В итоге получаю должность Director of blockchain development и контракт (не smart, а legal), который мне и принёс в итоге указанные в заголовке деньги. Как оказалось – это был самый прибыльный способ для меня заработать в крипте, а не все эти пи-ту-пи или мои кроссовки Stepn).

Естественно, работа получена – проект заброшен. Вернулся я к нему только в 2025м. И то, если бы не lovable [9], я бы не продолжал – застопорился на UI, а эту часть я прям не фанат разгребать. Lovable сделал шаблон, я его расширил на приложение. Сейчас переписал бы фронт с нуля, потому что lovable + tailwind_css = архитектурная дичь.

ой, кто-то на стене Артемия Лебедева написал "ХХЙ"

ой, кто-то на стене Артемия Лебедева написал “ХХЙ”

Оптимизации

Дисклеймер_1. Весь код и все рассуждения, приведенные ниже, действительны изначально для моего проекта, моих структур данных, и моего способа использования данных. На ваших структурах и бизнес логике [10] мои хитрости могут не сработать.

A. Время нужно делить

Это был прям взрыв мозга [11]!

Мне нужно было запилить календарь в 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 [12] при 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 [13]. Однако, когда система начала разрастаться, я заметил, что в рамках одного запроса часто делаю много одинаковых проверок ролей и доступов, поскольку прыгаю из контракта в контракт. Тогда решил перейти на diamond-2535 [14] (я использую 3ю версию; обновил ее под новые версии solidity и hardhat здесь [15]). Пересмотрев еще раз архитектуру приложения, пару контрактов оставил под proxy-1967, основную часть перевел на diamond-2535, а еще пару оставил вообще без прокси (бекон [16] и accessControl).

Как итог – снижение потребления газа на основные (тяжелые) функции на 20%, а на некоторых мелких участках замеров – до 80%.

Г. Двусвязный список с якорями для длинных списков

Дисклеймер_2: я изучал алгоритмы и структуры данных 15+ лет назад в универе. Однако ни разу за 15 лет работы это не потребовалось. Только в прошлом году, возможно, для некоторых фич моего solidity кода эти алгоритмы могли бы пригодиться. До сих пор считаю, что ради 1 раза в 15 лет хватит посмотреть ютуба разок или чисто из головы что-то придумать. Поэтому если ниже я описал очередной велосипед – ну, сорян, можете пролистать.

Пример, как я “придумал” систему хранения данных

В начале моей solidity карьеры я придумал, что для сохранения storage layout при замене контрактов можно хранить данные в отдельном месте как key-value, а через какое-то время оказалось, что это называется eternal storage [17] и давно существует.

Так вот. Были у меня списки комментариев к страницам. Список 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

Источник [18]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/33389

URLs in this post:

[1] Solidity: https://www.soliditylang.org/

[2] Raffle: https://github.com/artem-bayandin/blockchain-samples/tree/master/raffle

[3] On-chain NFT: https://github.com/artem-bayandin/blockchain-samples/tree/master/upgradeable-onchain-nft

[4] спецификации OpenSea: https://docs.opensea.io/docs/metadata-standards

[5] tokenURI: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/cfe2f2f608dd86e8458297919de93b0b03e3da22/contracts/token/ERC721/ERC721.sol#L76

[6] Shept: https://chromewebstore.google.com/detail/shept/neinfkegbabifhjnjjimmnadbooacgpi

[7] whitepaper: https://phantom-coach-fd6.notion.site/Shept-Whitepaper-29624b24459a806db554ef7e6c84b34e

[8] metamorphic: https://github.com/0age/metamorphic

[9] lovable: https://lovable.dev/

[10] логике: http://www.braintools.ru/article/7640

[11] мозга: http://www.braintools.ru/parts-of-the-brain

[12] remix: https://remix.ethereum.org/

[13] proxy-1967: https://docs.openzeppelin.com/contracts-stylus/erc1967

[14] diamond-2535: https://github.com/mudgen/diamond

[15] здесь: https://github.com/artem-bayandin/diamond-2535

[16] бекон: https://habr.com/ru/articles/1014998/

[17] eternal storage: https://fravoll.github.io/solidity-patterns/eternal_storage.html

[18] Источник: https://habr.com/ru/articles/1054440/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1054440

www.BrainTools.ru

Rambler's Top100