Сеть Ethereum, широко известная в узком кругу блокчейн-разработчиков, уже зарекомендовала себя как удобная и стабильная платформа для разработки смарт-контрактов. Мы стараемся сделать смарт-контракты доступными для неподготовленных пользователей, предлагая простые, но практически полезные контракты. Недавно мы разработали смарт-контракт спора Bet Me. В основе контракта лежит пари (спор) двух оппонентов. Они подкрепляют уверенность в собственной правоте денежной ставкой. Проигравший теряет деньги, а победитель забирает всё. Подробнее о нём я расскажу в этой статье.
Зачем здесь блокчейн?
Для начала — вопрос, которым редко задаются авторы статей о блокчейне: а нужен ли в этой ситуации блокчейн? Какие задачи сложно решить при помощи устной договорённости или юридического договора, но просто — при помощи блокчейна?
Если два человека поспорили устно, это часто выливается в новое разбирательство. «Я не это имел в виду», «На результат повлияли внешние обстоятельства, иначе я бы оказался прав», «Да это я не всерьёз с тобой спорил, а ты себе понапридумывал» и прочие возможные отговорки делают устный спор уделом хорошо знакомых людей при достаточно низких ставках. При серьёзных ставках и относительно далёком знакомстве более перспективно заключить письменное соглашение и детально прописать в нём суть спора, ставки, критерии принятия решения и любые другие условия, которые стороны считают важными.
У этого подхода есть ряд недостатков.
- Чаще всего необходимо привлечь юриста, а то и двух, по одному с каждой стороны, иначе можно забыть важный пункт, и это приведёт к финансовым потерям.
- Нередко в договоре прописываются только обязанности сторон, а ответственность за нарушение этих обязанностей — нет. В итоге одной стороне оказывается слишком легко не платить, а другой — крайне сложно и дорого получить свои деньги через суд.
- А может быть так, что обе стороны настаивают на своей правоте, и даже обращение в суд не гарантирует, что победителю достанутся деньги.
- Может оказаться и так, что у проигравшей стороны денег просто нет, и даже решение суда не заставит её расплатиться по долгам, несмотря на все обязательства.
Блокчейн же (и сеть Ethereum в частности) позволяет работать с деньгами (будем называть эфир деньгами; это не совсем корректно, но удобно и в достаточной степени отражает истинное положение вещей, поскольку эфир достаточно легко обменять на фиатные деньги, и наоборот). В то же время в Ethereum можно свести договорённости к набору конкретных правил, не выполнить их просто не получится. Итак, наш смарт-контракт принимает от каждой стороны деньги и блокирует их до наступления конкретных событий. Набор заданных программных правил позволит победителю вывести деньги. Это как раз то, что нужно.
Реализация
Набор правил, регулирующих спор, можно реализовать множеством способов. Ниже речь пойдёт о том, что сделали мы для пользователей платформы Smartz.
В споре участвуют две стороны. Тот, кто создаёт экземпляр контракта в сети Ethereum, называется владельцем контракта (Owner), а его противник — оппонентом (Opponent). Владелец контракта задаёт текстовое утверждение, которое он считает истинным. Оппонент делает ставку на то, что утверждение ложно. Решение о результате спора принимает независимый арбитр (Arbiter), кандидатуру которого утверждают и владелец, и оппонент. Арбитр получает комиссию в виде процента от суммы спора.
Работа контракта разделена на несколько последовательных стадий.
-
Переговоры. Владелец и оппонент ещё до создания контракта могут любым удобным способом вести переговоры. Совместно решив, кто будет арбитром, они присылают кандидату приглашение рассудить их спор. Получив приглашение, арбитр увидит все условия и соответствующий State. Подробнее об этом ниже, а пока важно понимать, что это число будущий арбитр должен передать в контракт, чтобы показать, на каких условиях он готов рассудить спорщиков. Если владелец установил ненулевую сумму залога (ArbiterPenaltyAmount), то, соглашаясь с условиями, арбитр должен перечислить указанную сумму эфира в контракт, после чего она блокируется, пока арбитр не рассудит спорщиков либо пока не наступит крайняя дата (Deadline) для разрешения спора. В последнем случае арбитр теряет возможность вывести залог, и эта сумма распределяется поровну между участниками спора.
-
Инициализация. Владелец контракта создаёт экземпляр контракта и настраивает его параметры: предмет спора; дату, до которой арбитр должен принять решение (Deadline); процент комиссии арбитра (дробное число ≥ 0 и < 100); сумму залога (может быть нулевой), которую арбитр должен внести как гарантию того, что он берётся рассудить спор в имеющейся формулировке к нужному времени. Владелец также задаёт Ethereum-адрес арбитра, которому он доверяет. Только обладатель данного адреса сможет позже стать арбитром.
-
Ставка владельца. После настройки владелец контракта делает ставку. Для этого он присылает любую сумму эфира в контракт. Эта сумма и есть ставка, она блокируется на адресе контракта.
-
Согласие арбитра. Ставка владельца фиксирует условия спора. Теперь арбитр видит полные условия сделки: формулировку спора, время до которого нужно принять решение, и главное, может понять, сколько эфира он получит в качестве вознаграждения. Если арбитра все устраивает, он подтверждает свое участие и одновременно перечисляет страховой депозит.
-
Поиск оппонента. После согласия арбитра начинается поиск оппонента. Владелец заранее задаёт адрес оппонента, если готов спорить только с кем-то конкретным, либо оставляет адрес пустым, и тогда оппонентом может стать владелец любого адреса в сети (кроме арбитра и владельца). Оппонент подтверждает участие в споре, вызывая отдельный метод контракта, в который он передаёт текущий номер версии данных и эфир — столько же, сколько поставил владелец. С этого момента пари считается заключённым. Теперь контракт ждёт либо решения арбитра, либо наступления даты Deadline.
-
Исход спора. Арбитр может рассудить спор тремя способами.
— Признать утверждение верным. В этом случае владелец контракта может вывести всю сумму эфира, кроме комиссии арбитра и залоговой суммы (если она была): эти деньги выводит арбитр, а оппоненту не достаётся ничего.
— Признать утверждение ложным. В этом случае арбитр может вывести эфир в размере причитающейся ему комиссии и суммы залога. Оппонент забирает остальное, а владельцу не достаётся ничего.
— Признать спор неразрешимым. Например, владелец создал спор с утверждением «Футбольный матч между командами А и B, назначенный на ближайшее воскресенье, закончится со счетом 2:1 в пользу A». Если матч отменили, арбитр не разрешит спор, но он должен иметь возможность забрать свой залог, потому что проблема возникла не по его вине. Каждая из сторон в этом случае может запросить перевод эфира в размере собственной ставки с адреса контракта на свой кошелёк. -
Вывод средств. Когда арбитр принял решение либо наступила дата Deadline, каждая из сторон может запросить вывод эфира. Сколько эфира выводить, рассчитает сам контракт, ориентируясь на результаты спора.
-
Уничтожение контракта. Владелец может отправить в контракт команду самоуничтожения. Это можно сделать либо до заключения сделки (если арбитр не нашёлся), либо после её завершения (если все стороны вывели причитающиеся им средства). Такая возможность окажется полезна, если волшебным образом на адрес контракта поступило больше эфира, чем запланировано. Вероятность такого события очень низка, но всё же в 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сылки
- GitHub: BetMe smart contract https://github.com/smartzplatform/betme-ether
- BetMe — Smartz Platform Wiki https://wiki.smartz.io/betme
- Token-Curated Registries 1.0 (Управляемые токенами реестры 1.0) https://habr.com/company/mixbytes/blog/418711/
- Расширение MetaMask https://metamask.io
- Trust — Ethereum & ERC20 Wallet https://trustwalletapp.com
Автор: mxpaul