Ce este în neregulă cu swap-urile atomice și cum pot ajuta canalele, ce s-a întâmplat cu adevărat în hard fork-ul Constantinople și ce să faci când nu ai cu ce plăti gazul.
Motivația principală a oricărui specialist în securitate este dorința de a evita responsabilitatea.
Providența a fost binevoitoare, am părăsit ICO-ul fără să aștept prima tranzacție ireversibilă, dar curând m-am trezit lucrând la dezvoltarea unei burse de criptomonede.
Eu— cu adevărat nu sunt Măciuca Kibalciș, și o singură privire severă este suficientă pentru a-mi preda toate cheile și parolele. Prin urmare, scopul meu principal ca arhitect a fost să plasez asul incalzit al criptoanalizei cât mai departe de elementele infrastructurii care îmi sunt dragi.
Nu sunt cheile tale, nu sunt problemele tale.
Construim un sistem de schimb de active și vrem să excludem stocarea intermediară a acestor active la noi, dar trebuie să asigurăm securitatea tranzacției.
Poți acționa ca un judecător într-o situație disputată și să efectuezi tranzacții cu portofele care necesită două din trei semnături: cumpărător, vânzător și escrow.
Cu toate acestea, dacă un participant atacă cu succes escrow-ul, el obține cele două semnături necesare.
Swap-ul atomic este un mecanism de schimb în care garanția este un contract inteligent, care permite doar un comportament corect.
Asemenea enigmei cu lupul, capra și varza, poți acționa numai după un singur scenariu corect și suferi pierderi dacă te abați de la acesta.
Doar că în locul animalelor lacome, ordinea este asigurată de o funcție hash, în care este atât de greu de găsit o coliziune încât nu merită să începi.
Pasul întâi: enigma.
Să presupunem că Alice, într-o dimineață splendidă, vrea să îi transmită lui Bob un bitcoin în schimbul unei mânștiri de "crypto-yuan".
- Ea își pune un mare secret.
- Primește de la el un hash.
- Transferă bitcoinii pe un contract inteligent, din care banii pot fi luați de Bob, prezentând secretul (hash-ul său trebuie să fie egal cu cel specificat în contract).
- În cazul în care Bob nu se prezintă după bitcoinii săi până seara, Alice îi poate recupera înapoi.
Pasul doi: momeala.
Bob intră în joc și transferă "crypto-euro" pe contractul său, care este scris astfel încât:
- Alice poate lua "crypto-yenii" prezentând un număr secret.
- Nu mai devreme de prânz, Bob, în absența lui Alice, poate returna depozitul.
Pasul trei: descoperirea momelei.
Alice vine pentru banii ei și ia banii din contractul lui Bob, dezvăluind astfel secretul ei.
Pasul final: misterul este dezvăluit
Bob vede tranzacția și, cu o privire acvatică, extrage din ea secretul prezentat de Alice în contract. Acest secret îl folosește pentru a-și recupera bitcoin-urile.
Când ceva nu merge bine
Dacă Alice se dovedește brusc a fi muritoare, Bob își ia yuan-ii în prânz.
La rândul ei, Alice returnează bitcoin-ul seara, dacă vicleanul Bob decide să păstreze banii pentru vremuri mai bune.
Dacă preferați imaginea textului, pe Habr pentru voi există o explicație mai detaliată și vizuală .
Diferența dintre timeout-uri este destinată să ne asigure împotriva lui Alice cea malefică, care ia banii lui Bob în ultimul moment, iar timeout-ul expiră în timp ce acesta tastează cu degete tremurânde hex-ul în tranzacție.
Participanții nu pot pierde banii lor, maxim va trebui să aștepte returnarea.
Suport în blockchain-uriEste un sistem simplu, ca o pantofă, care cere de la blockchain-urile interacționante foarte puțin:
- Suport pentru contracte inteligente cu cel puțin o ramificație
- Ambele blockchain-uri trebuie să susțină aceleași algoritmi de hashing (nu uitați să verificați lungimea secretului)
- Timp de blocare.
La prima vedere, deja putem spune bursei „adio, întâlnirea noastră a fost o greșeală”, dar nu este așa.
În ciuda tuturor avantajelor sale, soluțiile bazate pe swap atomic nu strălucesc prin lichiditate. În mare parte pentru că, în cea mai populară pereche BTC-USD, partea fiat nu a fost complet tokenizată.
Succesul USDT a generat o întreagă avalanșă de monede stabile de format ERC20 pentru toate gusturile, de la cel mai custodial USDC până la cel mai algoritmic DAI.
De aceea, pentru simplitate, discutăm în continuare despre faptul că Alice îi vinde lui Bob bitcoin pentru anumite token-uri ERC20 și sperăm la succesul stabilizatorilor, bineînțeles că mai avem multe probleme tehnice.
Viteză
Bitcoin și Ethereum, de unul singur, nu sunt foarte rapide, iar acum trebuie să așteptăm mai întâi un depozit cu toate confirmările, apoi al doilea.
Totul se datorează faptului că mai întâi banii sunt depuși de participantul care cunoaște secretul, iar adversarul așteaptă finalitatea și abia apoi își transferă partea.
În plus, ne ocupăm de un activ foarte volatil, așa că, în această perioadă, cursul poate suferi modificări semnificative, iar schimbarea condițiilor devine complicată.
Confidențialitate
Fiecare schimb lasă artefacte pe ambele blockchain-uri. Un observator atent poate observa hash-uri identice în contractele inteligente și poate deduce logic că a avut loc o tranzacție, ceea ce poate genera numeroase concluzii, de la variații de curs până la cele fiscale.
Când bursa știe despre afacerile tale — este foarte neplăcut, iar când toată lumea știe — este neplăcut de două ori.
Utilizabilitate
Trucul blockchain-ului în general și al etereului în special. Haideți să vedem ce mișcări trebuie să facă vânzătorul și cumpărătorul.
Din perspectiva vânzătorului, totul este relativ simplu: trebuie doar să transfere Bitcoin pe o adresă p2sh. Cu Ethereum, lucrurile sunt mult mai complicate.
ContractSă luăm în considerare un contract mediu pe GitHub pentru swap:
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 if 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);
}
}
Atenție! Nu folosiți acest contract și altele din articol pe producție, ele sunt scrise exclusiv pentru demonstrație. În special acesta.
- Bob trebuie să apeleze metoda
approve, oferind contractului swap acces la tokenii săi - Bob creează swap-ul și contractul folosind metoda
transferFromcare îi ia tokenii expeditorului pe adresa sa - Alice în
withdrawdezlociu secretul și contractul invocătransfer
Majoritatea portofelelor și schimburilor de criptomonede nu suportă approve tokenii, și nu degeaba.
Utilizatorii greșesc adesea și pur și simplu trimit tokenii pe contract, după care tokenii se pierd în mod simplu. Comentariile de pe Etherscan sunt pline de plângeri ale celor nefericiți.
Și pentru a invoca contractul, trebuie să plătești o taxă în ETH, ceea ce înseamnă că ambele părți trebuie să aibă ETH înainte de a începe tranzacția, iar puțini sunt cei care vor să se ocupe de asta.
Gazgolder
Mai întâi, ar trebui să eliminăm verificarea expeditorului oriunde este posibil și să presupunem că avem pe cineva care suferă de exces de gaz și invocă contracte pentru toți doritorii.
Contract modernizat
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); // folosește hash odată
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);
}
}
Dualism contractual-cheie și EIP 712
Așa cum știm, adresa în Ethereum poate fi un contract sau un subiect, adică o cheie.
Principala activitate a cheii este de a semna diferite mesaje.
Putem folosi ca expeditor contractul Bob, care efectuează toate transferurile necesare, verificând mai întâi semnătura cheii Bob.
Acum, oricine poate sponsoriza taxa participatului, dar decizia o ia doar cel care cunoaște cheia.
Contractul Bob
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);
}
}
Pentru a lucra cu semnături ale structurilor de date complexe în Ethereum există un standard , mai multe despre acesta puteți citi în
Împarte și cucerește
Adesea, scenariul unui hack al unui contract Ethereum arată astfel:
- Un participant depune fonduri în contract
- Apoi retrage fondurile
- Ceva nu merge bine
- Atacatorul ia banii din nou și din nou
Dacă ne întoarcem la primul nostru exemplu, ceva nu merge bine când misterul este un set gol de octeți.
Cum să furi un milionCreăm un swap cu hash-ul 0x66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925
Acesta este sha256 de 0x0000000000000000000000000000000000000000000000000000000000000000
Transmitem secretul și ne luăm token-urile
Transmitem din nou și ne luăm altele, totul pentru că 0 = 0
Creând un contract separat pentru fiecare tranzacție, putem izola contractele la nivel EVM.
Dar asta nu este tot: acum fiecare tranzacție are propria adresă, la care pot fi transferate token-uri din orice portofel sau schimb.
Contracte abandonate și create2
Dar acum, pentru fiecare tranzacție, trebuie să creăm un contract și să așteptăm ca cumpărătorul să transfere acolo ”crypto-fenicul” lucrat. În schema „dimineața contractele, seara banii” există întotdeauna riscul ca cumpărătorul să renunțe, iar eterul pentru crearea contractului să fie deja cheltuit.
Nu se poate face astfel încât dimineața banii, iar seara octeți?
În hardfork-ul Constantinople, dezvoltatorii au adăugat instrucțiunea create2, care creează un nou contract la o adresă deterministă
keccak256( 0xff ++ address ++ salt ++ keccak256(init_code))[12:]
Unde
- address — adresa contractului fabricii
- salt — un număr oarecare, sensul căruia îl vom afla în următoarea serie
- init_code — bytecode-ul contractului și parametrii constructorului.
FabricaInstrucțiunea funcționează doar prin assembly, așa că fabrica arată destul de înfricoșător:
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);
}
}
Codul contractului tău poate fi obținut folosind 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);
Din cauza suportului limitat în solidity, gazul pentru contract poate fi calculat greșit din cauza unor subtilități ale eterului.
În special, este plăcut că în cazul lipsei de gaz, contractul se prăbușește cu o eroare internă, fără a semnala că nu a fost suficient gaz, cum s-ar putea aștepta.
Acum putem traduce tokeni pe contracte fără a le crea în prealabil și până când nu le publicăm în rețea, nimeni nu va bănui ce face de fapt contractul.
Ciorile nu își scot ochii.
Este clar că un analist adevărat, mai ales unul care a primit investiții bune pentru a lupta cu inamicii regimului prin spălarea banilor, nu va fi oprit de aceste trucuri infantile, iar după crearea contractului el va vedea totuși hash-ul.
Cum să facem astfel încât hash-ul să nu fie expus?
Swap-ul în sine îl mutăm în off-chain: participanții își schimbă semnăturile pentru a face transfer pe contractul de swap, iar apoi secretul este dezvăluit privat.
Pas cu pasSe creează două "multisig-uri", de unde pot fi retrase fonduri cu semnăturile lui Alice și Bob.
Pentru a face ca plecarea offline a oricărui participant să nu devină o tragedie, vom adăuga un vechi bun timeout.
Alice și Bob depun simultan garanții.
- Alice alege un secret și îi dă lui Bob hash-ul secretului și semnătura tranzacției care transferă bitcoin pe adresa swap-ului.
- Bob îi dă lui Alice semnătura pentru retragerea tokenilor pe contractul de swap cu hash-ul ales.
- Alice îi comunică lui Bob secretul.
În acel moment apare armonia: atât Alice, cât și Bob pot încheia afacerea în orice moment. Într-un astfel de mediu prietenos, ei se pot schimba semnăturile pentru retragerea banilor pe adresele finale.
Pentru un observator extern, acest lucru pare că banii au trecut printr-un contract cu semnătură multiplu 2 din 2.
În plus, acest sistem permite ambelor părți să facă depozitul simultan, deoarece secretul este ales deja după toate confirmările.
Nivelul 2
Deoarece putem retrage bani pe o singură adresă și nu publicăm tranzacția intermediară, nimic nu ne împiedică să retragem bani pe mai multe adrese și să efectuăm un număr nelimitat de tranzacții intermediare. Nu că ar fi un set necesar pentru schimb, dar dacă ai început să aduni swap-uri, este greu să te oprești.
Acum Alice și Bob vor putea să își desfășoare activitatea cu brio. De exemplu, să calculeze automat prețul mediu, schimbând pe satoshi pe secundă, sau pur și simplu să conecteze direct market maker-ul și recepționerul de lichiditate.
Pas cu pas
- Vânzătorul alege un secret și îi dă cumpărătorului hash-ul secretului și semnătura tranzacției în care o parte din fonduri sunt transferate pe adresa p2sh a swap-ului, iar restul se întoarce pe adresa vânzătorului.
- Clientul transmite o semnătură care permite transferul de tokeni și restul către adresa destinatarului.
- Vânzătorul dezvăluie un secret.
- Povestea se repetă cu un nou secret, adăugând în swap și restul și transferul anterioare achiziționate la adresa cumpărătorului și deja plătite la adresa vânzătorului.
Acum avem acces la comerțul p2p de mare viteză, cheia este să fim atenți la timp și să închidem tranzacția înainte de expirare.
Totuși, puțin ajustând contractele noastre, putem oferi canalelor noastre nemurire, ceea ce ne va simplifica mult crearea rețelei.
Dar despre asta vom vorbi în următorul episod.
Sursa: habr.com
