Czym są atomowe wymiany i jak mogą im pomóc kanały, co ważnego wydarzyło się w hard forku Constantinople i co zrobić, gdy nie ma się czym zapłacić za gaz.
Główną motywacją każdego specjalisty ds. bezpieczeństwa jest pragnienie uniknięcia odpowiedzialności.
Opatrzność była łaskawa, opuściłem ICO, nie czekając na pierwszą nieodwracalną transakcję, ale wkrótce znalazłem się przy tworzeniu giełdy kryptowalut.
Nie jestem w żadnym wypadku Młotkiem Kibałczyszem i jedno surowe spojrzenie wystarczy, abym podał wszystkie klucze i hasła. Dlatego moim głównym celem jako architekta było umiejscowienie gorącego żądła kryptanalizy jak najdalej od drogich mi rzeczy infrastruktury.
Nie twoje klucze, nie twoje problemy.
Budujemy system wymiany aktywów i chcemy wyeliminować pośrednie przechowywanie tych aktywów u nas, ale musimy zapewnić bezpieczeństwo transakcji.
Można pełnić rolę sędziego w spornej sytuacji i przeprowadzać transakcje z portfelami, które wymagają dwóch z trzech podpisów: kupującego, sprzedającego i escrows.
Jednak jeśli uczestnik skutecznie zaatakuje escrow, otrzymuje pożądane dwa podpisy.
Atomowa wymiana to schemat wymiany, w którym gwarantem jest kontrakt smart, który dopuszcza tylko uczciwe zachowanie.
Jak w zagadce o wilku, kozie i kapuście, można działać tylko według jednego, właściwego scenariusza i ponosi się straty, jeśli się od niego odstępuje.
Tylko zamiast pożerających zwierząt porządek zapewnia funkcja hash, w której tak trudno znaleźć kolizję, że nie warto nawet zaczynać.
Krok pierwszy: zagadka.
Załóżmy, że Alicja pewnego pięknego poranka chce przekazać Bobo bitcoin za garść „kryptojuanów”.
- Zagaduję jakiś duży sekret.
- Otrzymuje od niego hash.
- Przenosi bitcoiny na kontrakt smart, z którego pieniądze może odebrać Bob, przedstawiając sekret (hash z niego musi być równy temu podanemu w umowie).
- W przypadku, gdy Bob nie odbierze swoich bitcoinów do wieczora, Alicja może je zabrać z powrotem dla siebie.
Krok drugi: przynęta.
W grę wchodzi Bob i przekazuje „krypto euro” na swój kontrakt, który jest napisany tak, że:
- Alicja może zabrać „krypto jeny”, przedstawiając sekretne liczby.
- Najwcześniej po obiedzie Bob, w przypadku nieobecności Alicji, może zwrócić depozyt.
Krok trzeci: rozwiązanie w przynęcie.
Alicja przychodzi po swoje pieniądze i odbiera pieniądze z kontraktu Boba, ujawniając przy tym swój sekret.
Ostatni krok: zagadka rozwiązana
Transakcję widzi Bob i bacznie wyłuskuje z niej sekret, przedstawiony Alicji w kontrakcie. Używa tego sekretu, aby zabrać swoje Bitcoiny.
Kiedy coś idzie nie tak
Jeśli Alicja nagle okaże się śmiertelna, Bob zabiera swoje juany na obiad.
Z kolei Alicja zwraca Bitcoin pod wieczór, jeśli zdradziecki Bob postanowi wstrzymać pieniądze do lepszych czasów.
Jeśli wolisz obrazek od tekstu, na Habrze znajdziesz bardziej szczegółowe i obrazowe .
Różnica między limitami czasowymi ma na celu zabezpieczenie nas przed złośliwą Alicją, która zabiera pieniądze Boba w ostatniej chwili, a limit czasowy upływa, podczas gdy on drżącymi palcami wpisuje hex do transakcji.
Uczestnicy nie mogą stracić swoich pieniędzy, najwyżej będą musieli poczekać na zwrot.
Wsparcie w blockchainachTo prosta jak przysłowiowy kamień schemat, który wymaga od współdziałających blockchainów dosłownie niewiele:
- Wsparcie dla smart kontraktów z przynajmniej jednym rozgałęzieniem
- Obydwa blockchainy muszą wspierać te same algorytmy haszujące (nie zapomnij sprawdzić długości sekretu)
- Locki czasowe.
Na pierwszy rzut oka, już można powiedzieć giełdzie „żegnaj, nasze spotkanie było błędem”, ale nie tak szybko.
Pomimo wszystkich swoich zalet rozwiązania oparte na atomic swap nie zachwycają płynnością. W dużej mierze dlatego, że w najpopularniejszej parze BTC-USD część fiatowa nie została w pełni tokenizowana.
Sukces USDT stworzył całą falę stabilnych monet w formacie ERC20 na każdy gust, od zarządzanych USDC po bardziej algorytmiczne DAI.
Dlatego dla uproszczenia zakładamy, że Alicja sprzedaje Bobowi Bitcoiny za jakieś tokeny ERC20 i liczymy na powodzenie stabilizatorów, tym bardziej, że mamy jeszcze wiele bardziej technicznych problemów.
Szybkość
Bitcoin i Ethereum, nawet osobno, nie są zbyt szybkie, a tu musimy najpierw poczekać na jeden depozyt ze wszystkimi potwierdzeniami, a potem na drugi.
To wszystko dlatego, że najpierw środki wpłaca uczestnik, który zna sekret, a przeciwnik czeka na finalizację i dopiero potem przelać swoją część.
Ponadto mamy do czynienia z bardzo zmiennym aktywem, więc w tym czasie kurs może się znacznie zmienić, a zmiana warunków jest już trudna.
Prywatność
Każda wymiana pozostawia ślady na obu łańcuchach bloków. Uważny obserwator może zauważyć te same hashe w smart kontraktach i wyciągnąć logiczny wniosek, że doszło do transakcji, z czego można wyciągnąć wiele wniosków, od kursów po podatki.
Kiedy giełda wie o twoich sprawach, to jest to bardzo nieprzyjemne, a jeśli wie o tym każdy, to jest to podwójnie nieprzyjemne.
Użyteczność
To, co wyróżnia blockchain w ogóle, a Ethereum w szczególności. Rzućmy okiem, jakie kroki musi podjąć sprzedawca i kupujący.
Z punktu widzenia sprzedawcy wszystko jest stosunkowo proste: wystarczy przelać bitcoin na adres p2sh. Z Ethereum jest to znacznie bardziej skomplikowane.
KontraktRozważmy średni kontrakt na githubie dotyczący swapu:
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); // sprawdź, czy swap o danym hashu już istnieje
require(token.transferFrom(msg.sender, address(this), amount)); // przetransferuj zablokowane tokeny do kontraktu swap
swaps[msg.sender][receiver] = Swap(token, hash, amount, refundTime, 0x00); // utwórz 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 istnieje
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);
}
}
Uwaga! Nie używaj tego ani innych kontraktów z artykułu w produkcji, są one napisane wyłącznie w celach demonstracyjnych. Szczególnie ten.
- Bob musi wywołać metodę kontraktu tokena
approve, aby dać kontraktowi swap dostępu do swoich tokenów - Bob tworzy swap i kontrakt za pomocą metody
transferFromodbiera tokeny od nadawcy na swój adres - Alicja w
withdrawujawnia sekret i wywołuje kontrakttransfer
Większość portfeli i giełd kryptowalut nie obsługuje approve tokenów, i nie bez powodu.
Użytkownicy często popełniają błędy i po prostu wysyłają tokeny na kontrakt, po czym tokeny po prostu znikają. Komentarze na Etherscan są pełne narzekań nieszczęśliwych.
Aby wywołać kontrakt, trzeba zapłacić opłatę w ETH, co oznacza, że obie strony muszą się w niego zaopatrzyć przed rozpoczęciem transakcji, a niewiele osób chce się tym zajmować.
Gazgolder
Na początek warto usunąć sprawdzenie nadawcy wszędzie, gdzie to możliwe, i założyć, że mamy kogoś, kto cierpi z powodu nadmiaru gazu i wywołuje kontrakty dla wszystkich chętnych.
Zmodernizowany kontrakt
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);
}
}
Dualizm kontrakt-klucz i EIP 712
Jak wiemy, adres w Ethereum może być kontraktem lub podmiotem, innymi słowy kluczem.
Główne zadanie klucza — podpisywanie różnych wiadomości.
Możemy użyć jako nadawcy kontraktu Boba, który wykonuje wszystkie niezbędne ruchy, wcześniej sprawdzając podpis klucza Boba.
Teraz każdy może sponsorować opłatę dla uczestnika, ale decyzję podejmuje tylko ten, kto zna klucz.
Kontrakt Boba
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);
}
}
Aby pracować z podpisami złożonych struktur danych w Ethereum, istnieje standard , więcej na ten temat można przeczytać w
Podział i rządzenie
Często scenariusz ataku na kontrakt Ethereum wygląda następująco:
- Uczestnik wpłaca środki do kontraktu
- Następnie wypłaca środki
- Coś poszło nie tak
- Złodziej zabiera pieniądze znowu i znowu
Jeśli wrócimy do naszego pierwszego przykładu, coś poszło nie tak, jeśli zagadką jest pusty zestaw bajtów.
Jak ukraść milionTworzymy swap z hashem 0x66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925
To jest sha256 od 0x0000000000000000000000000000000000000000000000000000000000000000
Przekazujemy sekret i odbieramy swoje tokeny
Przekazujemy jeszcze raz i odbieramy cudze, wszystko z powodu, że 0 = 0
Tworząc oddzielny kontrakt dla każdej transakcji, możemy izolować kontrakty na poziomie EVM.
Ale to nie wszystko: teraz każda transakcja ma swój adres, na który można przekazać tokeny z każdego portfela lub giełdy.
Porzucone kontrakty i create2
Ale teraz musimy tworzyć kontrakt dla każdej transakcji i czekać, aż kupujący przekaże tam swój „kryptofinansowy” wkład. W schemacie „rano kontrakty, wieczorem pieniądze” zawsze istnieje ryzyko, że kupujący odpadnie, a ether na stworzenie kontraktu już został wydany.
Czy nie można zrobić tak, że rano pieniądze, a wieczorem bajty?
W hardforcie Constantinople deweloperzy dodali instrukcję create2, która tworzy nowy kontrakt pod określonym adresem
keccak256( 0xff ++ address ++ salt ++ keccak256(init_code))[12:]
Gdzie
- address — adres kontraktu fabryki
- salt — jakaś liczba, której sens poznamy w następnej serii
- init_code — kod bajtowy kontraktu i parametry konstruktora.
FabrykaInstrukcja działa tylko przez assembly, dlatego fabryka wygląda nieco przerażająco:
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);
}
}
Kod twojego kontraktu można uzyskać przy pomocy web3:
const MyContract = new web3.eth.Contract(ABI, {})
const code = MyContract.deploy({
data: BYTECODE,
arguments: contructorArgs
}).encodeABI();
const factory = new web3.eth.Contract(FACTORY_ABI, factoryAddress);
tx = factory.methods.create2(code, salt);
Z powodu ograniczonego wsparcia w solidity, gaz dla kontraktu może być obliczany nieprawidłowo z powodu pewnych niuansów etheru.
Szczególnie miłe jest to, że w przypadku braku gazu kontrakt kończy się wewnętrznym błędem, nie informując przy tym, że gazu zabrakło, jak można by się tego spodziewać.
Teraz możemy przekazywać tokeny do kontraktów, nie tworząc ich wcześniej, a dopóki ich nie opublikujemy w sieci, nikt się nie domyśli, co dokładnie robi kontrakt.
Kruk krukowi oka nie wykole.
Oczywiście prawdziwego analityka, szczególnie takiego, który otrzymał dobre inwestycje w walkę z wrogami reżimu poprzez pranie pieniędzy, takie dziecięce sztuczki nie powstrzymają, i po stworzeniu kontraktu i tak zobaczy hash.
Jak sprawić, aby hash się nie ujawnił?
Sam swap przenosimy do off-chain: uczestnicy wymieniają podpisy na przelew do kontraktu swap, a następnie prywatnie ujawnia się sekret.
Krok po krokuTworzone są dwa “multisigi”, z których można odebrać środki przy obecności podpisów Alicji i Boba.
Aby wyjście offline któregoś z uczestników nie stało się tragedią, dodamy starą dobrą funkcję timeout.
Alicja i Bob równocześnie wpłacają depozyty
- Alicja wymyśla sekret i przekazuje Bobowi hash sekretu oraz podpis transakcji, która przekazuje bitcoiny na adres swapu.
- Bob przekazuje Alicji podpis na wypłatę tokenów na kontrakt swap z wymyślonym hashem.
- Alicja informuje Boba o sekrecie.
W tym momencie następuje harmonia: zarówno Alicja, jak i Bob mogą w każdej chwili zakończyć transakcję. W tak przyjaznej atmosferze mogą wymienić się podpisami, aby wypłacić pieniądze na końcowe adresy.
Dla zewnętrznego obserwatora wygląda to tak, jakby pieniądze przeszły przez kontrakt z multipodpisem 2 z 2.
A taka schemat umożliwia obu stronom jednoczesne wpłacanie depozytu, ponieważ sekret wymyślany jest dopiero po wszystkich potwierdzeniach.
Poziom 2
Skoro możemy przekazywać pieniądze na jeden adres i nie publikować pośredniej transakcji, nic nie przeszkadza nam przekazywać pieniędzy na kilka adresów i przeprowadzać nieograniczoną liczbę pośrednich transakcji. To nie jest konieczny zestaw do wymiany, ale jeśli zacząłeś zbierać swap, trudno jest przestać.
Teraz Alicja i Bob będą mogli się naprawdę rozwinąć. Na przykład, automatycznie obliczać średnią cenę, wymieniając po satoshi na sekundę, lub po prostu bezpośrednio połączyć market makera z odbiorcą płynności.
Krok po kroku
- Sprzedawca wymyśla sekret i przekazuje kupującemu hash sekretu oraz podpis transakcji, gdzie część środków jest przekazywana na adres p2sh swapu, a reszta wraca na adres sprzedawcy.
- Kupujący przesyła podpis umożliwiający wymianę tokenów i zwrot na adres odbiorcy.
- Sprzedawca ujawnia sekret
- Historia się powtarza z nowym sekretem, przy czym do wymiany i zwrotu dodawane jest jeszcze wypłacenie wcześniej zakupionych tokenów na adres kupującego oraz już opłaconych na adres sprzedawcy.
Teraz mamy dostęp do szybkiej wymiany p2p, ważne jest, aby pilnować czasu i zamknąć transakcję przed upływem czasu.
Jednak troszkę modyfikując nasze kontrakty, możemy nadać naszym kanałom nieśmiertelność, co znacznie uprości nam tworzenie sieci.
Ale o tym opowiemy w następnej serii.
Źródło: habr.com
