Чем плохи атомарные свопы и как каналы им помогут, что важного произошло в хардфорке Constantinople и как быть, когда нечем платить за газ.

Главная мотивация любого специалиста по безопасности— желание избежать ответственности.

Провидение было милостиво, я покинул ICO, не дожидаясь первой необратимой транзакции, но вскоре обнаружил себя за разработкой криптобиржи.

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

Не твои ключи, не твои проблемы


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

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

Однако, если участник успешно атакует эскроу, то он получает искомые две подписи.

Атомарный своп— схема обмена, где гарантом выступает смарт-контракт, который допускает только честное поведение.

Словно в загадке про волка козу и капусту ты можешь действовать только по единственному правильному сценарию и несешь потери, если отступаешь от него.

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

Шаг первый: загадка


Предположим, что Алиса в одно прекрасное утро хочет передать Бобу биткоин за горсть “криптоюаней”.

  • Она загадывает какой-то большой секрет
  • Получает от него хэш
  • Переводит биткоины на смарт контракт, забрать с которого деньги может Боб, предъявив секрет (хэш от него должен быть равен указанному в контракте)
  • В случае, если Боб не является за своими биткоинами к вечеру, Алиса может забрать их обратно себе.

Шаг второй: приманка


В игру вступает Боб и переводит“криптоевро” на свой контракт, который написан таким образом что:

  • Алиса может забрать “криптойены” предъявив секретное число
  • Не ранее обеда Боб, при неявке Алисы может вернуть депозит

Шаг третий: отгадка в приманке


Алиса приходит за своими деньгами и забирает деньги с контракта Боба, раскрыв при этом свой секрет.

Шаг завершающий: загадка разгадана


Транзакцию видит Боб, и орлиным взором вычленяет из нее секрет, предъявленный Алисой контракту. Этот секрет он использует, чтобы забрать уже свои биткоины.

Когда что-то идет не так


Если Алиса вдруг оказывается внезапно смертна, Боб в обед забирает свои юани.

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

Если вы предпочитаете картинку тексту, на Хабре для вас есть более подробное и наглядное объяснение работы атомарных свопов.

Разница между таймаутами призвана застраховать нас от зловредной Алисы, которая забирает деньги Боба в самый последний момент, и таймаут истекает, пока тот дрожащими пальцами вбивает hex в транзакцию.

Участники не могут потерять свои деньги, максимум, придется подождать возврата.

Поддержка в блокчейнах
Это простая, как валенок, схема, которая требует от взаимодействующих блокчейнов всего ничего:

  • Поддержка смарт контрактов с хотя бы одним ветвлением
  • Оба блокчейна должны поддерживать одинаковые алгоритмы хэширования (не забывайте проверять длину секрета)
  • Таймлоки.


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

При всех своих достоинствах решения на atomic swap не поражают ликвидностью. Во многом потому, что в самой популярной паре BTC-USD фиатная часть была не вполне токенизирована.
Успех USDT породил целую волну стабильных монет формата ERC20 на любой вкус, от кастодиальнейшего USDC до алгоритмичнейшего DAI.

Поэтому для простоты мы рассуждаем далее о том, что Алиса продает Бобу биткоины за какие-то ERC20 токены, и надеемся на удачу стабилизаторов, благо у нас еще много более технических проблем.

Скорость


Биткоин и Ethereum и по отдельности не слишком быстры, а тут нам приходится ждать сначала один депозит со всеми подтверждениями, потом второй.

Это все потому, что сначала деньги вносит участник, которому известен секрет, а оппонент ждет финальности и только затем переводит свою часть.

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

Конфиденциальность


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

Когда о твоих делах знает биржа --это крайне неприятно, когда об этом знает каждый — это неприятно вдвойне.

Usability


Конек блокчейна вообще и эфира в частности. Давайте посмотрим, какие телодвижения придется совершить продавцу и покупателю.

С точки зрения продавца все относительно просто: нужно просто перевести биткоин на p2sh адрес. С эфиром все гораздо хитрее.

Контракт
Рассмотрим усредненный по гитхабу контракт для свопа:

contract iERC20 {
    function totalSupply() public view returns (uint256);
    function transfer(address receiver, uint numTokens) public returns (bool);
    function balanceOf(address tokenOwner) public view returns (uint);
    function approve(address delegate, uint numTokens) public returns (bool);
    function allowance(address owner, address delegate) public view returns (uint);
    function transferFrom(address owner, address buyer, uint numTokens) public returns (bool);
}

contract Swapper {

    struct Swap {
        iERC20 token;
        bytes32 hash;
        uint amount;
        uint refundTime;
        bytes32 secret;
    }

    mapping (address => mapping(address => Swap)) swaps;

    function create(iERC20 token, bytes32 hash, address receiver, uint amount, uint refundTime) public {
        require(swaps[msg.sender][receiver].amount == 0); // check is swap with given hash already exists
        require(token.transferFrom(msg.sender, address(this), amount)); // transfer locked tokens to swap contract
        swaps[msg.sender][receiver] = Swap(token, hash, amount, refundTime, 0x00); //create swap
    }
    
    function hashOf(bytes32 secret) public pure returns(bytes32) {
        return sha256(abi.encodePacked(secret));
    }


    function withdraw(address owner, bytes32 secret) public {
        Swap memory swap = swaps[owner][msg.sender];
        require(swap.secret == bytes32(0));
        require(swap.hash == sha256(abi.encodePacked(secret))); // swap exists
        swaps[owner][msg.sender].secret = secret;
        swap.token.transfer(msg.sender, swap.amount);
    }

    function refund(address receiver) public {
        Swap memory swap = swaps[msg.sender][receiver];
        require(now > swap.refundTime);
        delete swaps[msg.sender][receiver];
        swap.token.transfer(msg.sender, swap.amount);
    }
}

Внимание! Не используйте этот и другие контракты из статьи на продакшене, они написаны исключительно для демонстрации. Особенно этот.

  • Боб должен вызвать у контракта токена метод approve, дав контракту свопа доступ к своим токенам
  • Боб создает своп и контракт при помощи метода transferFrom забирает на свой адрес токены отправителя
  • Алиса в withdraw раскрывает секрет и контракт вызывает transfer

Большинство кошельков и криптобирж не поддерживают approve токенов, и не зря.

Сами пользователи часто ошибаются и просто переводят токены на контракт, после чего токены просто теряются. Комментарии на Etherscan полны стенаниями несчастных.

А чтобы вызвать контракт, нужно заплатить комиссию в ETH, значит оба участника должны запастись им перед началом сделки, а этим мало кто хочет заниматься.

Газголдер


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

Модернизированный контракт
contract Swapper {

    struct Swap {
        iERC20 token;
        address receiver;
        uint amount;
        address refundAddress;
        uint refundTime;
    }

    mapping (bytes32 =>  Swap) swaps;

    function create(iERC20 token, bytes32 hash, address receiver, uint amount, address refundAddress, uint refundTime) public {
        require(swaps[hash].amount == 0); // use hash once
        require(token.transferFrom(msg.sender, address(this), amount));
        swaps[hash] = Swap(token, receiver, amount, refundAddress, refundTime);
    }


    function withdraw(bytes memory secret) public {
        bytes32 hash = sha256(secret);
        Swap memory swap = swaps[hash];
        require(swap.amount > 0);
        delete swaps[hash];
        swap.token.transfer(swap.receiver, swap.amount);
    }

    function refund(bytes32 hash) public {
        Swap memory swap = swaps[hash];
        require(now > swap.refundTime);
        delete swaps[hash];
        swap.token.transfer(swap.refundAddress, swap.amount);
    }
}


Контрактно-ключевой дуализм и EIP 712


Как мы знаем, адрес в эфире может быть контрактом, а может быть субъектом, сиречь ключем.
Главное занятие ключа— подписывать какие-нибудь сообщения.

Мы можем использовать в качестве отправителя Боб-контракт, который совершает все необходимые пассы, проверив перед этим подпись Боба-ключа.

Теперь, кто угодно может спонсировать комиссию участника, но принимает решение только тот, кому известен ключ.

Боб-контракт
library EIP712ProxyLibrary {
    function hashCommand(address sender, iERC20 token, Swapper swapper, bytes32 hash, address receiver, uint amount, address refundAddress, uint refundTime) public view returns(bytes32);
}

contract ProxyBob {
    address owner;

    constructor(address _owner) public {
        owner = _owner;
    }

    function createSwap(Swapper swapper, iERC20 token, bytes32 hash, address receiver, uint amount, address refundAddress, uint refundTime, uint8 v, bytes32 r, bytes32 s) public {
        require(owner == ecrecover(EIP712ProxyLibrary.hashCommand(address(this), token, swapper, hash, receiver, amount, refundAddress, refundTime), v, r, s));
        token.approve(address(swapper), amount);
        swapper.create(token, hash, receiver, amount, refundAddress, refundTime);
    }
}


Для работы с подписями сложных структур данных в Ethereum есть стандарт EIP 712, подробнее о нем вы можете прочитать в блоге кошелька Metamask

Разделяй и властвуй


Часто сценарий взлома Ethereum контракта выглядит так:

  • Участник кладет средства на контракт
  • Потом забирает средства
  • Что-то идет не так
  • Злоумышленник забирает деньги снова и снова

Если мы вернемся к нашему первому примеру, что-то идет не так, если загадкой является пустой набор байт.

Как украсть миллион
Создаем своп с хэшем 0x66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925
Это sha256 от 0x0000000000000000000000000000000000000000000000000000000000000000
Передаем секрет и забираем свои токены
Передаем еще раз и забираем чужие, все из-за того что 0 = 0

Создавая для каждой сделки отдельный контракт, мы можем изолировать контракты на уровне EVM.

Но и это еще не все: теперь каждая сделка имеет свой адрес, на который можно перевести токены с любого кошелька или биржи.

Брошенные контракты и create2


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

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

В хардфорке Constantinople разработчики EIP 1014 добавили инструкцию create2, которая создает новый контракт на детерминированном адресе

keccak256( 0xff ++ address ++ salt ++ keccak256(init_code))[12:]

Где

  • address — адрес контракта фабрики
  • salt — какое-то число, смысл которого мы узнаем в следующей серии
  • init_code — байт-код контракта и параметры конструктора.

Фабрика
Инструкция работает только через assembly, поэтому фабрика выглядит несколько устрашающе:

contract Factory {
  event Deployed(address addr, uint256 salt);

  function create2(bytes memory code, uint256 salt) public {
    address addr;
    assembly {

      addr := create2(0, add(code, 0x20), mload(code), salt)
    }

    emit Deployed(addr, salt);
  }
}

Код вашего контракта можно получить при помощи web3:

const MyContract = new web3.eth.Contract(ABI, {})
const сode = MyContract.deploy({
    data: BYTECODE,
    arguments: contructorArgs  
}).encodeABI();
const factory = new web3.eth.Contract(FACTORY_ABI, factoryAddress);
tx = factory.methods.create2(сode, salt);

Из-за ограниченной поддержке в solidity газ для контракта может расчитываться неправильно из-за некоторых тонкостей эфира.

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

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

Ворон ворону глаз не выклюет


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

Как сделать так, чтобы хэш не засветился?

Сам своп мы переносим в офчейн: участники обмениваются подписями для перевода на своп-контракт, а затем приватно раскрывается секрет.

Шаг за шагом
Создаются два “мультисига”, с которых можно забрать средства при наличии подписей Алисы и Боба.

Дабы уход в оффлайн кого-либо из участников не стал трагедией, добавим старый добрый таймаут.

Алиса и Боб параллельно вносят депозиты

  • Алиса загадывает секрет и передает Бобу хэш секрета и подпись транзакции, которая переводит биткоины на адрес свопа
  • Боб передает Алисе подпись на вывод токенов на контракт свопа с загаданным хэшем.
  • Алиса сообщает Бобу секрет.


В этот момент наступает гармония: и Алиса и Боб могут в любой момент закончить сделку. В такой дружественной обстановке они могут обменяться подписями от для вывода денег на конечные адреса.

Для стороннего наблюдателя это выглядит как будто деньги прошли через контракт с мультиподписью 2 из 2.

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

Level 2


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

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

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


Теперь нам доступна высокоскоростная p2p торговля, главное следить за временем и закрыть сделку до таймаута.

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

Но об этом мы расскажем в следующей серии.