Начну с описания кейса, где нам потребовалось решение подобной задачи. Мы реализовали проект TheWall Global, в рамках которого мы создали двумерное пространство или стену, которая разделена на равные области, каждая из которых представляет собой NFT-токен. Любой желающий может приобрести себе кусочек нашего виртуального пространства, чтобы разместить там любую информацию, которую он считает интересной и полезной для других – эдакое не модерируемое пространство всего значимого для человечества и человека. Базовая идея была нами аккуратно заимствована из замечательного и всем известного проекта TheMillionDollarHomepage, но мы наделили эту историю замечательной вторичной экономикой. Не буду вдаваться, собственно, в детали проекта и полемизировать на тему того, интересно ли это, монетизируется ли оно, будет ли работать на длинной дистанции – скажу лишь, что за три недели мы напродавали областей уже почти на пол миллиона долларов и провели переговоры с десятками галерей и художников, которые горят желанием разместить свои экспозиции на нашем пространстве, поэтому, думаю будет интересно. Но сейчас мы не об этом.

В какой-то момент нам пришла в голову замечательная идея, геймифицировать процесс покупки областей, сделав его более интересным для покупателей. Мы решили начать с простой механики, которая с моей легкой руки получила название – Кладоискатели. Суть механики предельно проста, в какой-то момент времени мы «загадываем» счастливые области, просто выбирая их из числа свободных на этот момент областей, даем покупателям, скажем, неделю, чтобы купить себе одну или несколько областей, а затем сообщаем всем, какие именно области мы загадали, а если кто-то приобрел какие-то из них, то он получает приз в виде некоторого количества токенов (например, токенов USDT или других ERC20-токенов). Все бы ничего, но нам очень захотелось реализовать эту механику именно децентрализованным способом. Зачем? Ну, как минимум, чтобы исключить обвинения в наш адрес, что мы изменили загаданные области, чтобы не расставаться с призом.

Децентрализованная реализация предполагала наличие отдельного смартконтракта, который мы назвали Treasure (пер. с англ. – Сокровище). Но тут возникла загвоздка. Чтобы исключить подтасовку, нам нужно было как-то сообщить смартконтракту координаты счастливых областей до начала игры, но это означало бы, что нам пришлось бы сообщить всем желающим правильный ответ. Решение, которое мы применили, оказалось очень простым. Мы решили загружать в смартконтракт не координаты счастливых областей, а рассчитанную от них хеш-функцию, т.е. «прячем сокровище» мы простым вызовом функции:

    function hideTreasure(bytes32 hash) onlyOwner public
    {
        _hash = hash;
        emit TreasureHidden(hash);
    }

Мы использовали хеш-функцию keccak256, т.к. ее вызов доступен прямо из смартконтракта, а следовательно, смартконтракт может убедиться, что загружаемые впоследствии нами (организаторами ) координаты счастливых областей, в точности соответствуют загаданным до начала игры. Ну и, как видите, вызов функции hideTreasure() не раскрывает секрета, представляющего собой набор координат областей, заданный в виде массива int256 [] coords, представляющего собой последовательность (x1, y1, x2, y2, …. , xN, yN).

Вроде все выглядит хорошо, но на самом деле, не очень. Стена у нас имеет ограниченный размер – 1000x1000 областей, т.е. всего 1 миллион вариантов размещения, да еще и за вычетом уже занятых областей. Согласитесь, не такая уж сложная задача для полного перебора координат и расчета их хеша. Разумеется при не очень больших N. Поэтому мы решили не рисковать и немного «подсолить» вектор координат, добавив в его конец еще один член – SALT – очень большое и очень случайное число. Получилась последовательность (x1, y1, x2, y2, …. , xN, yN, SALT), для которой мы уже и считали хеш.

Подведение итогов игрового раунда выглядит тоже весьма компактно и просто:

    function findTreasure(int256 [] memory coords, uint256 salt) onlyOwner public
    {
        bytes32 hash = keccak256(abi.encodePacked(keccak256(abi.encodePacked(coords)), salt));
        require(hash == _hash, "Treasure: Invalid data");
        emit TreasureFound(coords);
        delete _hash;

        for(uint i = 0; i < coords.length;)
        {
            int256 x = coords[i++];
            int256 y = coords[i++];
            uint256 tokenId = _thewallcore._areaOnTheWall(x, y);
            if (tokenId != 0)
            {
                address user = _thewall.ownerOf(tokenId);
                if (_treasureContract.transfer(user, _treasuredAmount))
                {
                    emit Rewarded(user, tokenId, _treasuredAmount);
                }
            }
        }
    }

Собственно, самое интересно в приведенном выше коде находится в первой строчке функции, которая описывает, как посчитать значение хеш-функции keccak256 практически от любых параметров. В нашем случае мы считали хеш от массива координат abi.encodePacked(coords), но, в принципе, на этом месте мог бы быть секрет совершенно любого типа и формата. Подробнее про abi.encodePacked можно почитать в документации на Solidity или посмотреть наглядные примеры вот в этой статье.

Ну а далее, убедившись, что загружаемые координаты соответствуют загаданным на момент начала игры, т.е. хеш от них, соответствует загруженному ранее, а значит организатор не изменил условия задачи, смартконтракт пробегает по всем заданным координатам, проверяет, есть у соответствующих им областей владелец, ну и, если есть, начисляет ему заслуженную награду, которая должна быть предусмотрительно размещена на адресе самого смартконтракта Treasure.

Наш проект работает на базе блокчейна Polygon (MATIC), но все описанное мной выше прекрасно может быть исполнено и на Ethereum и многих других, сделанных на базе кода Ethereum блокчейнов. Почему мы остановились именно на Polygon-е – тема для отдельной истории, как и множество других лайфхаков, которые мы использовали при создании нашего проекта. Возможно, я расскажу о них в последующих постах. Но это не точно :)

Полный текст смартконтракта Treasure доступен тут.  Возможно, вам будет интересно взглянуть и на другие контракты нашего проекта, которые вызываются из приведенного выше в качестве примера кода, а именно, смартконтракты TheWall и TheWallCore.

Сам проект доступен по адресу https://thewall.global, а сопроводительная информация на сайтах https://thewall.cptpoint.global и https://about.thewall.global.

Комментарии (4)


  1. leviaxma
    28.10.2021 22:39

    1. isvirin Автор
      28.10.2021 22:44

      Нам тоже ) Я об этом в начале статьи честно написал))


  1. vovanmozg
    21.11.2021 07:09

    SALT вы раскрываете после игры, верно?


    1. isvirin Автор
      21.11.2021 11:11

      Разумеется. Без SALT смартконтракт не сможет проверить, что загружено именно исходное задание, которое не поменялось в процессе игры.