Сеть Ethereum, широко известная в узком кругу блокчейн-разработчиков, уже зарекомендовала себя как удобная и стабильная платформа для разработки смарт-контрактов. Мы стараемся сделать смарт-контракты доступными для неподготовленных пользователей, предлагая простые, но практически полезные контракты. Недавно мы разработали смарт-контракт спора Bet Me. В основе контракта лежит пари (спор) двух оппонентов. Они подкрепляют уверенность в собственной правоте денежной ставкой. Проигравший теряет деньги, а победитель забирает всё. Подробнее о нём я расскажу в этой статье.


Зачем здесь блокчейн?


Для начала — вопрос, которым редко задаются авторы статей о блокчейне: а нужен ли в этой ситуации блокчейн? Какие задачи сложно решить при помощи устной договорённости или юридического договора, но просто — при помощи блокчейна?


Если два человека поспорили устно, это часто выливается в новое разбирательство. «Я не это имел в виду», «На результат повлияли внешние обстоятельства, иначе я бы оказался прав», «Да это я не всерьёз с тобой спорил, а ты себе понапридумывал» и прочие возможные отговорки делают устный спор уделом хорошо знакомых людей при достаточно низких ставках. При серьёзных ставках и относительно далёком знакомстве более перспективно заключить письменное соглашение и детально прописать в нём суть спора, ставки, критерии принятия решения и любые другие условия, которые стороны считают важными.


У этого подхода есть ряд недостатков.


  • Чаще всего необходимо привлечь юриста, а то и двух, по одному с каждой стороны, иначе можно забыть важный пункт, и это приведёт к финансовым потерям.
  • Нередко в договоре прописываются только обязанности сторон, а ответственность за нарушение этих обязанностей — нет. В итоге одной стороне оказывается слишком легко не платить, а другой — крайне сложно и дорого получить свои деньги через суд.
  • А может быть так, что обе стороны настаивают на своей правоте, и даже обращение в суд не гарантирует, что победителю достанутся деньги.
  • Может оказаться и так, что у проигравшей стороны денег просто нет, и даже решение суда не заставит её расплатиться по долгам, несмотря на все обязательства.

Блокчейн же (и сеть Ethereum в частности) позволяет работать с деньгами (будем называть эфир деньгами; это не совсем корректно, но удобно и в достаточной степени отражает истинное положение вещей, поскольку эфир достаточно легко обменять на фиатные деньги, и наоборот). В то же время в Ethereum можно свести договорённости к набору конкретных правил, не выполнить их просто не получится. Итак, наш смарт-контракт принимает от каждой стороны деньги и блокирует их до наступления конкретных событий. Набор заданных программных правил позволит победителю вывести деньги. Это как раз то, что нужно.


Реализация


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


В споре участвуют две стороны. Тот, кто создаёт экземпляр контракта в сети Ethereum, называется владельцем контракта (Owner), а его противник — оппонентом (Opponent). Владелец контракта задаёт текстовое утверждение, которое он считает истинным. Оппонент делает ставку на то, что утверждение ложно. Решение о результате спора принимает независимый арбитр (Arbiter), кандидатуру которого утверждают и владелец, и оппонент. Арбитр получает комиссию в виде процента от суммы спора.


Работа контракта разделена на несколько последовательных стадий.


  1. Переговоры. Владелец и оппонент ещё до создания контракта могут любым удобным способом вести переговоры. Совместно решив, кто будет арбитром, они присылают кандидату приглашение рассудить их спор. Получив приглашение, арбитр увидит все условия и соответствующий State. Подробнее об этом ниже, а пока важно понимать, что это число будущий арбитр должен передать в контракт, чтобы показать, на каких условиях он готов рассудить спорщиков. Если владелец установил ненулевую сумму залога (ArbiterPenaltyAmount), то, соглашаясь с условиями, арбитр должен перечислить указанную сумму эфира в контракт, после чего она блокируется, пока арбитр не рассудит спорщиков либо пока не наступит крайняя дата (Deadline) для разрешения спора. В последнем случае арбитр теряет возможность вывести залог, и эта сумма распределяется поровну между участниками спора.
  2. Инициализация. Владелец контракта создаёт экземпляр контракта и настраивает его параметры: предмет спора; дату, до которой арбитр должен принять решение (Deadline); процент комиссии арбитра (дробное число ? 0 и < 100); сумму залога (может быть нулевой), которую арбитр должен внести как гарантию того, что он берётся рассудить спор в имеющейся формулировке к нужному времени. Владелец также задаёт Ethereum-адрес арбитра, которому он доверяет. Только обладатель данного адреса сможет позже стать арбитром.
  3. Ставка владельца. После настройки владелец контракта делает ставку. Для этого он присылает любую сумму эфира в контракт. Эта сумма и есть ставка, она блокируется на адресе контракта.
  4. Согласие арбитра. Ставка владельца фиксирует условия спора. Теперь арбитр видит полные условия сделки: формулировку спора, время до которого нужно принять решение, и главное, может понять, сколько эфира он получит в качестве вознаграждения. Если арбитра все устраивает, он подтверждает свое участие и одновременно перечисляет страховой депозит.
  5. Поиск оппонента. После согласия арбитра начинается поиск оппонента. Владелец заранее задаёт адрес оппонента, если готов спорить только с кем-то конкретным, либо оставляет адрес пустым, и тогда оппонентом может стать владелец любого адреса в сети (кроме арбитра и владельца). Оппонент подтверждает участие в споре, вызывая отдельный метод контракта, в который он передаёт текущий номер версии данных и эфир — столько же, сколько поставил владелец. С этого момента пари считается заключённым. Теперь контракт ждёт либо решения арбитра, либо наступления даты Deadline.
  6. Исход спора. Арбитр может рассудить спор тремя способами.
    — Признать утверждение верным. В этом случае владелец контракта может вывести всю сумму эфира, кроме комиссии арбитра и залоговой суммы (если она была): эти деньги выводит арбитр, а оппоненту не достаётся ничего.
    — Признать утверждение ложным. В этом случае арбитр может вывести эфир в размере причитающейся ему комиссии и суммы залога. Оппонент забирает остальное, а владельцу не достаётся ничего.
    — Признать спор неразрешимым. Например, владелец создал спор с утверждением «Футбольный матч между командами А и B, назначенный на ближайшее воскресенье, закончится со счетом 2:1 в пользу A». Если матч отменили, арбитр не разрешит спор, но он должен иметь возможность забрать свой залог, потому что проблема возникла не по его вине. Каждая из сторон в этом случае может запросить перевод эфира в размере собственной ставки с адреса контракта на свой кошелёк.
  7. Вывод средств. Когда арбитр принял решение либо наступила дата Deadline, каждая из сторон может запросить вывод эфира. Сколько эфира выводить, рассчитает сам контракт, ориентируясь на результаты спора.
  8. Уничтожение контракта. Владелец может отправить в контракт команду самоуничтожения. Это можно сделать либо до заключения сделки (если арбитр не нашёлся), либо после её завершения (если все стороны вывели причитающиеся им средства). Такая возможность окажется полезна, если волшебным образом на адрес контракта поступило больше эфира, чем запланировано. Вероятность такого события очень низка, но всё же в Ethereum нельзя полностью заблокировать перевод эфира на адрес произвольного контракта, а бросать замороженные деньги глупо.

Теперь немного о том, зачем нужен State Version Number. Это число, которое увеличивается при каждом изменении значимых условий спора, таких как формулировка спора, размеры комиссий или штрафов арбитра. Когда кто-то соглашается с условиями спора, он 1) видит актуальное состояние данных; 2) отправляет вызов метода контракта, который регистрирует согласие с условиями. Если между этими двумя событиями одна из сторон (вероятнее всего, владелец) изменит параметр контракта, получится согласие с другой версией данных. Например, кандидат в арбитры заходит в интерфейс контракта на smartz.io и видит, что ему предлагают рассудить спор на 10 Ether (сегодня это около 3000 долларов) за комиссию 1 % (примерно 30 долларов). Кандидат с радостью соглашается и отправляет транзакцию с подтверждением в сеть. Нечестный владелец видит в пуле майнинга необработанную транзакцию арбитра и отправляет свою: меняет вознаграждение арбитра на 0 %. Мошенник ставит цену газа выше средней, и с некоторой долей вероятности майнеры могут обработать его транзакцию раньше. Такая атака называется Front running attack. State Version Number защищает от неё. Если транзакция владельца-вредителя будет обработана раньше, номер версии данных в контракте изменится. Арбитр в своей транзакции послал номер версии данных на единицу меньше. Поэтому контракт откажется выполнять транзакцию, произойдёт откат. Арбитр просмотрит новые условия и либо откажется от участия, либо согласится, отправив актуальный номер версии данных.


При разработке контракта, оперирующего эфиром, приходится много думать о плохих сценариях. Что будет, если владелец контракта решил обделить арбитра? А если арбитр оказался нечестным? А если оппонент на самом деле хакер? Или все трое жаждут обмануть друг друга, потому что ставки в споре достаточно высоки? Кроме того, необходимо учесть любые возможности нарушения нормального хода спора, когда эфир будет заблокирован в контракте и даже владелец не получит к нему доступ. Например, вариант признания спора неразрешимым возник уже в ходе реализации. По той же причине последовательность этапов спора именно такова: ставка владельца -> выбор арбитра -> ставка оппонента. Может случиться так, что оппонент не подтвердил своё участие, а время Deadline установлено далеко в будущем. Чтобы не превратиться в долгосрочного инвестора, арбитр может отказаться от участия, но только пока оппонент не сделал ставку. И таких нюансов в контракте много. Хорошая новость в том, что это надо запрограммировать один раз и использовать. Если бы спор был оформлен как бумажный договор, в случае таких пограничных ситуаций много людей раз за разом должны были бы вникать в каждый пункт и договариваться между собой, как его интерпретировать. Блокчейн позволяет зафиксировать условия, как в договоре, но возложить интерпретацию условий на виртуальную машину и всегда иметь один и только один результат их исполнения.


Нельзя обойти вниманием проблему заинтересованного арбитра. В нашем контракте арбитр принимает решение в одиночку. Для простых ситуаций этого достаточно, но иногда риск личной заинтересованности арбитра недопустим. Один из выходов — ввести в логику контракта возможность добавлять нескольких арбитров и принимать решение путём голосования. Это довольно сложная логика, особенно если захочется сделать её универсальной для всех возможных споров и их участников. Однако хорошая новость в том, что всю логику сложного коллективного арбитража можно вынести в отдельный смарт-контракт. Адрес контракта владелец спора пропишет в качестве арбитра. С точки зрения интерфейса такой контракт должен иметь возможность вызывать из контракта спора несколько методов: согласие судить спор, отказ от такого согласия, три версии решения и один метод вывода эфира. Внутри арбитражного контракта может использоваться логика принятия решения большинством арбитров наподобие того, как это сделано в контракте Multisignature Wallet, также доступном на Smartz.io в виде конструктора.


Для части споров можно заменить группу арбитров контрактом, использующим одного или нескольких оракулов. Или придумать другой выход, например превратить спор в рулетку со случайным решением. И всё это — без изменения кода контракта спора и без усложнения логики его работы.


Тестирование


Хочется сказать пару слов о тестировании. Все знают, что автоматизация тестирования — это хорошо. Многие на самом деле пишут тесты для своего кода. Кое-кто использует в разработке подход TDD — давно и хорошо известный Test Driven Development. Ключевое отличие TDD от простого тестирования в том, что тесты пишутся раньше, чем код. Это позволяет взглянуть на код контракта снаружи, прочувствовать возможные проблемы и заранее их решить. Кроме того, TDD при правильном использовании позволяет значительно быстрее изменить логику работы, если это потребуется внезапно. Правильное использование приходит с опытом. TDD не серебряная пуля, как можно подумать, читая многочисленные материалы на эту тему. В то же время вызывает беспокойство, что руководства по разработке в Truffle и Node.js вообще не демонстрируют использование TDD для разработки на Solidity. Начинающие разработчики приобретают неправильные привычки и в итоге много страдают.


TDD подразумевает, что тестов в проекте много. Например, код контракта спора занимает 325 строк, а код тестов для этого контракта состоит из 2144 строк. В какой-то момент тестов стало достаточно много, чтобы прогон truffle test занимал больше минуты. Цикл разработки в TDD подразумевает частый прогон тестов после небольших изменений кода. Чтобы разработка не превратилась в мучение, пришлось научить Truffle запускать только часть тестов, совпадающих с переданным регулярным выражением.


Под капотом Truffle использует для работы с тестами фреймворк Mocha. Mocha умеет фильтровать запуск тестов по регулярке, но Truffle из коробки не умеет передавать соответствующий параметр --grep из командной строки. Воспользовавшись тем, что конфиг Truffle — это обычный код на JavaScript, я вписал в него разбор аргументов командной строки и формирование параметров для Mocha. Конфиг, который у меня получился в итоге, доступен в GitHub проекта. Реализация не слишком красивая, но это работает и экономит кучу времени.


Резюме


Контракт спора был задуман как очень простой по функционалу, но благодаря TDD и анализу возможных векторов атаки эволюционировал в чуть более богатую на ограничения реализацию. Явные недостатки контракта связаны с единоличным решением арбитра, однако их можно устранить без модификации кода контракта спора, если реализовать в отдельном смарт-контракте систему голосования нескольких арбитров. Тем же способом реализуется использование оракулов для споров, в которых это возможно.


Контракт спора BetMe можно протестировать и запустить с помощью готового шаблона на платформе Smartz. Для этого потребуется расширение Metamask для десктопного браузера или Trust Wallet для мобильных устройств. Также исходный код самого контракта выложен на GitHub.


Стоит признать, что на сегодняшний день применение блокчейн-технологий сводится в основном к криптовалютам и выпуску токенов для ICO. Децентрализованные автономные организации (DAO) пока не стали реальностью. Но если пофантазировать, как дальше будут развиваться системы контрактов спора, то можно представить реестр арбитров с рейтингом, например на основе Token Curated Registry. После завершения споров их участники могли бы голосовать за или против арбитров, с которыми имели дело, изменяя их положение в рейтинге.


С одной стороны, контракт спора BetMe — практически применимый самодостаточный элемент. Но с другой стороны, он вполне может стать одним из кирпичиков, из которых со временем сложится экосистема децентрализованных организаций.


Cсылки


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


  1. omgiafs
    22.08.2018 17:47

    Ну шо, кто первый запилит букмекерские конторы без возможности обмана клиента, в обход налоговой и прочего обвеса? Бегите пейсать код, хипстеры, ваш час пробил!


    1. BoogerWooger
      23.08.2018 12:09

      welcome, по сложившейся традиции, первый пост в статье про блокчейн принадлежит хейтерам


    1. rPman
      23.08.2018 18:39

      bitshares prediction market существует уже несколько лет, с ui обычной биржи (стакан, торги,..), при этом возможно использование не криптовалют с высокой волатильностью (+-10% за сутки — норма) а обеспеченных стаблкоин, usd и cny даже с неплохой ликвидностью, грубо говоря если вам надо делать ставки на десятки тысяч баксов (можно и больше но нужно понимать что такое ликвидность, иначе можно заплатить высокую комиссию в момент проблем, если вдруг понадобится запросить обеспечение в обход рынка), то велкам.


  1. robert_ayrapetyan
    22.08.2018 19:45

    Почему бы не заменить арбитра (человека) в данной схеме на оракула (скрипт)?


    1. mxpaul Автор
      22.08.2018 23:50

      В статье так и написано

      Для части споров можно заменить группу арбитров контрактом, использующим одного или нескольких оракулов.

      Но в другой части споров этого сделать нельзя. Например, если я с другом (находясь, конечно, в гостинице) поспорил что он не сможет десять раз подряд зажечь свою зажигалку, оракула найти будет сложно. Легче найти ночного портье с кошельком в эфире.

      Фишка в том что адрес арбитра здесь это адрес в эфире. Это может быть адрес кошелька за которым стоит живой человек, или адрес другого смарт-контракта, который умеет вызывать из нас несколько методов. Как работат этот контракт-арбитр вопрос открытый. Можно сделать оракул, можно голосование группы людей, ну или что-то другое что придет в голову.


  1. Bloxy
    22.08.2018 21:17

    А чем это решение лучше чем Augur, Gnosis, Delphi? И это самые крупные — решений prediction market и betting тоже достаточно. Чем вы лучше?


    1. mxpaul Автор
      23.08.2018 00:09

      Названные это все же prediction market, и в статье нет упоминаний что данное решение лучше или хуже кого-то другого. Откуда вы делаете такие выводы?

      И кто «вы» лучше? Если вы это Smartz.io, то сложно сравнивать, Smartz это маркетплейс где можно найти контракт удовлетворяющий запросам пользователя и развернуть свой инстанс с готовой админкой не обладая навыками программирования на solidity.


  1. Luxo
    23.08.2018 09:28

    Почему ставки оппонентов всегда должны быть равны? По-моему это сильное упущение.
    Почему вывод средств нужно запрашивать отдельно, если можно организовать вывод сразу на основании транзакции арбитра с решением? Заодно и уничтожение контракта выполнить.
    Не проще ли вместо «State Version Number» просто запретить смену условий контракта?


    1. mxpaul Автор
      23.08.2018 11:59
      +1

      Почему ставки оппонентов всегда должны быть равны? По-моему это сильное упущение.

      Можно написать отдельный контракт который делает так. Просто не стояло такой задачи, поэтому сделали вариант наименее болезненный для конечного пользователя платформы. Ethereum, да и любая другая blockchain-сеть на сегодняшний день не слишком дружелюбны к пользователю. Людей которые понимают как этим пользоваться примерно как в начале 90-х было людей которые понимали как отправить электронную почту. Поэтому даже небольшое усложнение логики может стать препятствием на пути к использованию.

      Почему вывод средств нужно запрашивать отдельно, если можно организовать вывод сразу на основании транзакции арбитра с решением? Заодно и уничтожение контракта выполнить.

      Исполнение транзакции в ethereum стоит газа. Если сделать перечисление денег транзакцией арбитра, получится что за газ платит он, и от его комиссии съедается кусочек. При большой комисии это не проблема, но если арбитр зарабатывает понемногу на сотнях и тысячах споров, комиссия может стать ощутимой.

      Кроме того, есть вопросы безопасности. Например, может быть так что на адрес одной из сторон эфир перевести не получилось. Стандартный выход при такой ситуации — откат всей транзакции. Если, допустим, оппонент не принимает эфир, арбитр не сможет вывести свой процент и все деньги навечно зависнут в контракте.

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

      Не проще ли вместо «State Version Number» просто запретить смену условий контракта?

      Допустим я запускаю контракт спора с формулировкой «в декабре один биткойн будет стоить полмиллиона рублей». Ввожу дедлайн полгода, прописываю проценты, штрафы, и даже оппонета для спора нашел и вписал. Показываю контракт арбитру, он говорит все хорошо, только давай формулировку уточним: «в декабре 2018 один биткойн будет стоить не менее полумиллиона рублей». И тут выясняется что параметры контракта менять нельзя. Придется уничтожить этот контракт и запустить новый с другой формулировкой. Но зачем так делать, если можно достаточно безболезненно разрешить изменение параметров спора в ходе переговоров? Да, вводить state непривычно для сегодняшнего пользователя. Но так же непривычно для него то что нельзя редактировать параметры которые им же раньше были введены. Мы выбрали редактируемость, потому что вероятность того что пользователь ошибется в формулировке достаточно высока.


      1. BoogerWooger
        23.08.2018 12:20

        как раз пример того, что в теории алгоритм контракта кажется понятным и простым, но на деле приходится множество дополнительных решений принимать. Для реальных сделок приходится добавлять вспомогательный функционал, и его может быть даже больше, чем основного (взять например какой нить простейший вроде-бы invoice).
        Для этого контракта старались максимально упростить его для пользователя. Конечно можно было много всяких условий добавить и оракулы прикрутить, и следить за ними — только это ж отдельный проект. А у нас — конструкторы, которые, в идеале, должны быть представлены на платформе в десятке разных видов от разных разработчиков и с разным функционалом.


      1. Luxo
        23.08.2018 14:17

        Исполнение транзакции в ethereum стоит газа. Если сделать перечисление денег транзакцией арбитра, получится что за газ платит он, и от его комиссии съедается кусочек. При большой комисии это не проблема, но если арбитр зарабатывает понемногу на сотнях и тысячах споров, комиссия может стать ощутимой.
        Общая комиссия за газ в Вашем варианте больше. Разделение оплаты между всеми вроде как и честнее, но суммарно для участников менее выгодно.
        Кроме того, есть вопросы безопасности. Например, может быть так что на адрес одной из сторон эфир перевести не получилось. Стандартный выход при такой ситуации — откат всей транзакции. Если, допустим, оппонент не принимает эфир, арбитр не сможет вывести свой процент и все деньги навечно зависнут в контракте.
        Откат всей транзакции — это не стандартный выход, а стандартное поведение функции transfer(). Которое да — при неправильном использовании ведёт к багам. Стандартный же выход (ИМХО) — простая техника pendingWithdrawals.
        Придется уничтожить этот контракт и запустить новый с другой формулировкой. Но зачем так делать, если можно достаточно безболезненно разрешить изменение параметров спора в ходе переговоров?
        Спорный вопрос) Зачем усложнять код контракта, рискуя наплодить там дырок, если пользователю можно достаточно безболезненно просто создать новый контракт?


        1. mxpaul Автор
          23.08.2018 15:02

          Общая комиссия за газ в Вашем варианте больше. Разделение оплаты между всеми вроде как и честнее, но суммарно для участников менее выгодно.

          Я за честность. Арбитр не должен платить за перечисление эфира другим участникам. Ваш вариант тоже имеет право на жизнь, но, как сказал однажды в своем докладе greediness «Решили так».

          Откат всей транзакции — это не стандартный выход, а стандартное поведение функции transfer(). Которое да — при неправильном использовании ведёт к багам. Стандартный же выход (ИМХО) — простая техника pendingWithdrawals.

          revert это не стандартное, а единственное поведение функции transfer. Безревертный аналог называется send. Я стараюсь учитывать рекомендации по разработке беопасных смарт контрактов, consensys.github.io/smart-contract-best-practices/recommendations/#favor-pull-over-push-for-external-calls. В данном случае не вижу причины чтобы их нарушать.

          Погуглил по словам pendingWithdrawals и не нашел чего-то стандатрного. Можете описать алгоритм который имеете в виду или дать ссылку на стандарт? Для меня pending withdrawal это как раз вариант в котором тот кому причитается эфир сам отправляет транзакцию чтобы его забрать. Это и реализовано в контракте спора.

          Спорный вопрос) Зачем усложнять код контракта, рискуя наплодить там дырок, если пользователю можно достаточно безболезненно просто создать новый контракт?

          Согласен, вопрос спорный. Но как раз в этом случае мне показалось что удобство пользователя стоит некоторого усложнения логики.


          1. Luxo
            23.08.2018 15:59

            Погуглил по словам pendingWithdrawals и не нашел чего-то стандатрного. Можете описать алгоритм который имеете в виду или дать ссылку на стандарт? Для меня pending withdrawal это как раз вариант в котором тот кому причитается эфир сам отправляет транзакцию чтобы его забрать. Это и реализовано в контракте спора.
            что-то приблизительно такое:
            contract ContractPendingWithdrawals {
              mapping (address => uint) public pendingWithdrawals;
              function transfer_safe(address addr, uint amount) internal {
                if (!addr.send(amount)) {
                  pendingWithdrawals[addr] += amount;
                }
              }
              function withdrawPendingWithdrawal() external {
                amount = pendingWithdrawals[msg.sender];
                require(amount!=0);
                pendingWithdrawals[msg.sender] = 0;
                if (!msg.sender.call.value(amount)()) {
                  revert();
                }
              }
            }
            contract MyContract is ContractPendingWithdrawals {
              ...
              transfer_safe(arbiter, 1 Ether);
              transfer_safe(opponent, 10 Ether);
              ...
            }
            


            1. mxpaul Автор
              23.08.2018 16:16

              Спасибо. Общая идея мне нравится, хотя и остаюсь при мнении что в контакте спора вешать логику перевода эфира на решение арбитра это порочно.