Qué hay de malo en los swaps atómicos y cómo pueden ayudar los canales, qué eventos importantes ocurrieron en el hard fork Constantinople y qué hacer cuando no hay con qué pagar el gas.
La principal motivación de cualquier especialista en seguridad es el deseo de evitar responsabilidades.
La providencia fue generosa, dejé el ICO sin esperar la primera transacción irreversible, pero pronto me encontré desarrollando un intercambio de criptomonedas.
Yo definitivamente no soy Malchish Kibalchish, y con una sola mirada severa es suficiente para que entregue todas las claves y contraseñas. Por lo tanto, mi principal objetivo como arquitecto fue ubicar la ardiente aguijón del criptoanálisis lo más lejos posible de los elementos de infraestructura que me son queridos.
No tus claves, no tus problemas.
Estamos construyendo un sistema de intercambio de activos y queremos excluir el almacenamiento intermedio de estos activos, pero debemos garantizar la seguridad de la transacción.
Se puede actuar como juez en una situación controvertida y llevar a cabo transacciones con billeteras que requieren dos de tres firmas: la del comprador, la del vendedor y la del escrow.
Sin embargo, si un participante ataca exitosamente al escrow, obtiene las dos firmas deseadas.
Un swap atómico es un esquema de intercambio donde el garantizador es un contrato inteligente que permite solo un comportamiento honesto.
Como en el acertijo del lobo, la cabra y la col, solo puedes actuar de acuerdo al único escenario correcto y sufres pérdidas si te desvías de él.
Solo que en lugar de animales glotones, el orden es garantizado por una función hash, en la que es tan difícil encontrar colisiones que no vale la pena ni comenzar.
Paso uno: el acertijo.
Supongamos que Alice, una hermosa mañana, quiere enviar a Bob un bitcoin a cambio de un puñado de “criptoeuros”.
- Ella plantea algún gran secreto.
- Recibe de él un hash.
- Transfiere bitcoins a un contrato inteligente, del cual Bob puede retirar el dinero si presenta el secreto (el hash de este debe coincidir con lo indicado en el contrato).
- En caso de que Bob no venga por sus bitcoins en la tarde, Alice puede recuperarlos.
Paso dos: la trampa.
Bob entra en juego y envía “criptoeuros” a su contrato, que está redactado de tal manera que:
- Alice puede recuperar los “criptoyenes” presentando un número secreto.
- No antes del almuerzo Bob, en caso de que Alice no se presente, puede devolver el depósito.
Paso tres: adivinando la trampa.
Alicia viene a reclamar su dinero y retira el dinero del contrato de Bob, revelando su secreto.
Paso final: el enigma ha sido resuelto
Bob ve la transacción y con ojo de águila identifica el secreto que Alicia presentó en el contrato. Este secreto lo utiliza para reclamar sus propios bitcoins.
Cuando algo sale mal
Si Alicia resulta ser mortal de repente, Bob recogerá sus yuanes al mediodía.
A su vez, Alicia devuelve el bitcoin por la tarde, si el traicionero Bob decide retener el dinero hasta mejores momentos.
Si prefieres imágenes a texto, en Habr hay una explicación más detallada y visual para ti. .
La diferencia entre los tiempos de espera está destinada a protegernos de la maliciosa Alicia, que roba el dinero de Bob en el último momento, mientras él teclea temblorosamente el hex en la transacción.
Los participantes no pueden perder su dinero, a lo sumo tendrán que esperar la devolución.
Soporte en cadenas de bloquesEs un esquema tan sencillo como un zapato, que requiere de las cadenas de bloques interactivas lo mínimo:
- Soporte para contratos inteligentes con al menos una bifurcación
- Ambas cadenas de bloques deben soportar los mismos algoritmos de hash (no olviden verificar la longitud del secreto)
- Bloqueos temporales.
A primera vista, ya se podría decir al intercambio 'adiós, nuestro encuentro fue un error', pero no será tan fácil.
A pesar de todas sus ventajas, las soluciones basadas en swaps atómicos no deslumbran por su liquidez. En gran parte porque en el par más popular BTC-USD, la parte fiat no se había tokenizado del todo.
El éxito de USDT ha dado lugar a toda una ola de monedas estables en formato ERC20 para todos los gustos, desde el USDC más custodial hasta el DAI más algorítmico.
Por eso, para simplificar, hablaremos a continuación de que Alicia vende bitcoins a Bob por algunos tokens ERC20 y esperamos en la suerte de los estabilizadores, ya que todavía tenemos muchos problemas más técnicos.
Velocidad
Bitcoin y Ethereum, por separado, no son muy rápidos, y aquí tenemos que esperar primero un depósito con todas las confirmaciones, y luego el segundo.
Todo esto se debe a que primero un participante ingresa el dinero, el cual conoce el secreto, y el oponente espera la finalización y solo luego transfiere su parte.
Además, tratamos con un activo bastante volátil, por lo que durante este tiempo el precio puede cambiar significativamente, y alterar las condiciones ya no es sencillo.
Privacidad
Cualquier intercambio deja artefactos en ambas blockchains. Un observador atento puede notar los mismos hashes en los contratos inteligentes y llegar a la conclusión lógica de que se ha llevado a cabo una transacción, a partir de la cual se pueden hacer muchas inferencias, desde tasas hasta fiscales.
Cuando la bolsa conoce tus asuntos, es extremadamente desagradable; cuando todos lo saben, es doblemente desagradable.
Usabilidad
La esencia de la blockchain en general y de Ethereum en particular. Veamos qué movimientos deberán realizar el vendedor y el comprador.
Desde el punto de vista del vendedor, todo es relativamente simple: solo necesita transferir Bitcoin a la dirección p2sh. Con Ethereum, todo es mucho más complicado.
ContratoConsideremos un contrato promedio de GitHub para el 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 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);
}
}
¡Atención! No utilices este ni otros contratos del artículo en producción, están escritos únicamente para demostración. Particularmente este.
- Bob debe llamar al método del contrato del token
approve, dando al contrato swap acceso a sus tokens - Bob crea un swap y un contrato mediante el método
transferFromrecoge los tokens del remitente en su dirección - Alicia en
withdrawrevela un secreto y llama al contratotransferencia
La mayoría de las billeteras y exchanges de criptomonedas no soportan approve tokens, y no es por nada.
Los mismos usuarios a menudo cometen errores y simplemente envían tokens al contrato, después de lo cual los tokens se pierden. Los comentarios en Etherscan están llenos de lamentos de los desafortunados.
Y para llamar al contrato, es necesario pagar una tarifa en ETH, lo que significa que ambas partes deben tener ETH antes de comenzar la transacción, lo cual a casi nadie le apetece hacer.
Gasholder
Para empezar, es recomendable eliminar la verificación del remitente en todas partes donde sea posible, y suponer que hay alguien sufriendo de exceso de gas y llamando a contratos para todos los interesados.
Contrato modernizado
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);
}
}
Dualidad del contrato y la clave y EIP 712
Como sabemos, una dirección en Ethereum puede ser un contrato o puede ser un sujeto, es decir, una clave.
La principal función de la clave es firmar algunos mensajes.
Podemos usar a Bob-contractor como remitente, que realiza todos los pases necesarios, verificando antes la firma de la clave de Bob.
Ahora, cualquiera puede patrocinar la tarifa de un participante, pero solo decide aquel que conoce la clave.
Bob-contract
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);
}
}
Para trabajar con las firmas de estructuras de datos complejas en Ethereum hay un estándar , puedes leer más sobre él en
Divide y conquistarás
A menudo, el guion de un ataque a un contrato de Ethereum se ve así:
- Un participante deposita fondos en el contrato
- Luego retira los fondos
- Algo sale mal
- El atacante retira el dinero una y otra vez
Si volvemos a nuestro primer ejemplo, algo sale mal si el misterio es un conjunto vacío de bytes.
Cómo robar un millónCreamos un swap con el hash 0x66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925
Este es sha256 de 0x0000000000000000000000000000000000000000000000000000000000000000
Transmitimos el secreto y recuperamos nuestros tokens
Transmitimos una vez más y recuperamos los ajenos, todo porque 0 = 0
Al crear un contrato separado para cada transacción, podemos aislar los contratos a nivel de EVM.
Pero eso no es todo: ahora cada transacción tiene su dirección, a la cual se pueden transferir tokens desde cualquier billetera o intercambio.
Contratos desechados y create2
Pero ahora para cada transacción debemos crear un contrato y esperar a que el comprador transfiera allí su "crypto-fenching" arduamente ganado. En el esquema "contratos por la mañana, dinero por la tarde", siempre hay el riesgo de que el comprador se eche atrás, y el éter para crear el contrato ya se ha gastado.
¿No se puede hacer que por la mañana sea dinero y por la tarde bytes?
En el hard fork de Constantinople, los desarrolladores añadieron la instrucción create2, que crea un nuevo contrato en una dirección determinista
keccak256( 0xff ++ address ++ salt ++ keccak256(init_code))[12:]
Donde
- address — dirección del contrato de la fábrica
- salt — un número que descubriremos en el próximo episodio
- init_code — bytecode del contrato y parámetros del constructor.
FábricaLa instrucción funciona solo a través de assembly, por lo que la fábrica se ve un poco aterradora:
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);
}
}
Puedes obtener el código de tu contrato usando 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);
Debido al soporte limitado en solidity, el gas para el contrato puede calcularse incorrectamente debido a algunas sutilezas de éter.
Es especialmente curioso que en caso de falta de gas, el contrato falle con un error interno, sin informar que le faltó gas, como se podría esperar.
Ahora podemos traducir tokens a contratos sin crearlos de antemano y mientras no los publiquemos en la red, nadie se dará cuenta de lo que hace realmente el contrato.
El halcón no le saca los ojos a otro halcón.
Está claro que a un verdadero analista, especialmente si ha recibido buenas inversiones para combatir a los enemigos del régimen mediante el lavado de dinero, tales trucos infantiles no lo detendrán, y después de crear el contrato, aún verá el hash.
¿Cómo hacer para que el hash no se revele?
El intercambio lo llevamos a off-chain: los participantes intercambian firmas para la transferencia al contrato de intercambio, y luego se revela privadamente el secreto.
Paso a paso.Se crean dos 'multisig', desde los cuales se pueden retirar fondos con las firmas de Alicia y Bob.
Para que la salida a offline de cualquiera de los participantes no sea una tragedia, añadiremos un buen viejo timeout.
Alicia y Bob depositan fondos simultáneamente.
- Alicia elige un secreto y le pasa a Bob el hash del secreto y la firma de la transacción que transfiere bitcoins a la dirección del intercambio.
- Bob le pasa a Alicia la firma para retirar tokens en el contrato de intercambio con el hash elegido.
- Alicia le revela a Bob el secreto.
En ese momento se alcanza la armonía: tanto Alicia como Bob pueden finalizar el trato en cualquier momento. En este ambiente amistoso, pueden intercambiar firmas para retirar el dinero a las direcciones finales.
Para un observador externo, parece que el dinero ha pasado a través de un contrato con firma múltiple de 2 de 2.
Además, este esquema permite que ambas partes hagan un depósito simultáneamente, ya que el secreto se elige después de todas las confirmaciones.
Nivel 2.
Dado que podemos retirar dinero a una sola dirección y no publicar la transacción intermedia, no hay nada que nos impida retirar dinero a varias direcciones y realizar un número ilimitado de transacciones intermedias. No es que esto sea un conjunto necesario para el intercambio, pero una vez que has comenzado a recoger swaps, es difícil detenerse.
Ahora Alicia y Bob podrán lanzar su potencial. Por ejemplo, calcular automáticamente el precio medio, intercambiando a satoshis por segundo, o simplemente conectar directamente al creador de mercado y al receptor de liquidez.
Paso a paso.
- El vendedor elige un secreto y le da al comprador el hash del secreto y la firma de la transacción donde parte de los fondos se transfiere a la dirección p2sh del intercambio y el resto regresa a la dirección del vendedor.
- El comprador proporciona una firma que permite retirar tokens y el cambio a la dirección del destinatario.
- El vendedor revela un secreto
- La historia se repite con un nuevo secreto, añadiendo la retirada de lo previamente comprado a la dirección del comprador y ya pagado a la dirección del vendedor al swap y al cambio.
Ahora tenemos disponible comercio p2p de alta velocidad, lo principal es estar atentos al tiempo y cerrar el trato antes del tiempo de espera.
Sin embargo, al ajustar un poco nuestros contratos, podemos conceder a nuestros canales inmortalidad, lo que facilitará mucho la creación de la red.
Pero de esto hablaremos en el próximo episodio.
Fuente: habr.com
