- PVSM.RU - https://www.pvsm.ru -
Чем плохи атомарные свопы и как каналы им помогут, что важного произошло в хардфорке Constantinople и как быть, когда нечем платить за газ.
Главная мотивация любого специалиста по безопасности— желание избежать ответственности.
Провидение было милостиво, я покинул ICO, не дожидаясь первой необратимой транзакции, но вскоре обнаружил себя за разработкой криптобиржи.
Я— решительно не Мальчиш Кибальчиш, и одного строгого взгляда достаточно, чтобы я сдал все ключи и пароли. Поэтому главной моей целью как архитектора было расположить раскаленное жало криптоанализа как можно дальше от дорогих мне элементов инфраструктуры.
Мы строим систему обмена активами и хотим исключить промежуточное хранение этих активов у себя, но должны обеспечить безопасность сделки.
Можно выступить в качестве судьи в спорной ситуации и проводить сделки с кошельками, требующими две из трех подписей: покупателя, продавца и эскроу.
Однако, если участник успешно атакует эскроу, то он получает искомые две подписи.
Атомарный своп— схема обмена, где гарантом выступает смарт-контракт, который допускает только честное поведение.
Словно в загадке про волка козу и капусту ты можешь действовать только по единственному правильному сценарию и несешь потери, если отступаешь от него.
Только вместо прожорливых животных порядок обеспечивает хэш функция, в которой так сложно найти коллизию, что не стоит и начинать.
Предположим, что Алиса в одно прекрасное утро хочет передать Бобу биткоин за горсть “криптоюаней”.
В игру вступает Боб и переводит“криптоевро” на свой контракт, который написан таким образом что:
Алиса приходит за своими деньгами и забирает деньги с контракта Боба, раскрыв при этом свой секрет.
Транзакцию видит Боб, и орлиным взором вычленяет из нее секрет, предъявленный Алисой контракту. Этот секрет он использует, чтобы забрать уже свои биткоины.
Если Алиса вдруг оказывается внезапно смертна, Боб в обед забирает свои юани.
В свою очередь, Алиса к вечеру возвращает биткоин, если вероломный Боб решает придержать деньги до лучших времен.
Если вы предпочитаете картинку тексту, на Хабре для вас есть более подробное и наглядное объяснение работы атомарных свопов [1].
Разница между таймаутами призвана застраховать нас от зловредной Алисы, которая забирает деньги Боба в самый последний момент, и таймаут истекает, пока тот дрожащими пальцами вбивает hex в транзакцию.
Участники не могут потерять свои деньги, максимум, придется подождать возврата.
На первый взгляд, уже можно сказать бирже “прощай, наша встреча была ошибкой”, но не тут-то было.
При всех своих достоинствах решения на atomic swap не поражают ликвидностью. Во многом потому, что в самой популярной паре BTC-USD фиатная часть была не вполне токенизирована.
Успех USDT породил целую волну стабильных монет формата ERC20 на любой вкус, от кастодиальнейшего USDC до алгоритмичнейшего DAI.
Поэтому для простоты мы рассуждаем далее о том, что Алиса продает Бобу биткоины за какие-то ERC20 токены, и надеемся на удачу стабилизаторов, благо у нас еще много более технических проблем.
Биткоин и Ethereum и по отдельности не слишком быстры, а тут нам приходится ждать сначала один депозит со всеми подтверждениями, потом второй.
Это все потому, что сначала деньги вносит участник, которому известен секрет, а оппонент ждет финальности и только затем переводит свою часть.
Кроме того, мы имеем дело с весьма волатильным активом, так что за это время курс может весьма существенно измениться, а поменять условия уже непросто.
Любой обмен оставляет артефакты на обоих блокчейнах. Внимательный наблюдатель может заметить одинаковые хеши в смарт контрактах и сделать логичный вывод, что тут свершилась сделка, из чего можно произвести массу умозаключений от курсовых до налоговых.
Когда о твоих делах знает биржа --это крайне неприятно, когда об этом знает каждый — это неприятно вдвойне.
Конек блокчейна вообще вообще и эфира в частности. Давайте посмотрим, какие телодвижения придется совершить продавцу и покупателю.
С точки зрения продавца все относительно просто: нужно просто перевести биткоин на 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);
}
}
Как мы знаем, адрес в эфире может быть контрактом, а может быть субъектом, сиречь ключем.
Главное занятие ключа— подписывать какие-нибудь сообщения.
Мы можем использовать в качестве отправителя Боб-контракт, который совершает все необходимые пассы, проверив перед этим подпись Боба-ключа.
Теперь, кто угодно может спонсировать комиссию участника, но принимает решение только тот, кому известен ключ.
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 [2], подробнее о нем вы можете прочитать в блоге кошелька Metamask [3]
Часто сценарий взлома Ethereum контракта выглядит так:
Если мы вернемся к нашему первому примеру, что-то идет не так, если загадкой является пустой набор байт.
0x66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925
0x0000000000000000000000000000000000000000000000000000000000000000
Создавая для каждой сделки отдельный контракт, мы можем изолировать контракты на уровне EVM.
Но и это еще не все: теперь каждая сделка имеет свой адрес, на который можно перевести токены с любого кошелька или биржи.
Но теперь для каждой сделки нам приходится создавать контракт и ждать пока покупатель переведет туда трудовой “криптофенинг”. В схеме “утром контракты, вечером деньги” всегда есть опасность, что покупатель отвалится, а эфир на создание контракта уже потрачен.
Нельзя ли сделать так, чтобы утром деньги, а вечером байты?
В хардфорке Constantinople разработчики EIP 1014 [4] добавили инструкцию create2, которая создает новый контракт на детерминированном адресе
keccak256( 0xff ++ address ++ salt ++ keccak256(init_code))[12:]
Где
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.
А еще такая схема позволяет обеим сторонам делать депозит одновременно, так как секрет загадывается уже после всех подтверждений.
Раз мы можем выводить деньги на один адрес и не публиковать промежуточную транзакцию, ничего не мешает нам выводить деньги на несколько адресов и совершать неограниченное количество промежуточных транзакций. Не то что бы это был необходимый набор для обмена, но если начал собирать своп, трудно остановиться.
Теперь Алиса и Боб смогут развернуться вовсю. Например, автоматически высчитывать среднюю цену, обменивая по сатоши в секунду, или просто напрямую соединить маркетмейкера и получателя ликвидности.
Теперь нам доступен высокоскоростная p2p торговля, главное следить за временем и закрыть сделку до таймаута.
Однако, немного поправив наши контракты, мы можем подарить нашим каналам бессмертие, что сильно упростит нам создание сети.
Но об этом мы расскажем в следующей серии.
Автор: cloud
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/news/343085
Ссылки в тексте:
[1] объяснение работы атомарных свопов: https://habr.com/en/post/458646/
[2] EIP 712: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md
[3] блоге кошелька Metamask: https://medium.com/metamask/eip712-is-coming-what-to-expect-and-how-to-use-it-bb92fd1a7a26
[4] EIP 1014: https://eips.ethereum.org/EIPS/eip-1014
[5] Источник: https://habr.com/ru/post/483828/?utm_campaign=483828&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.