Intercambios Descentralizados (Decentralized Exchanges, DEXes) juegan una parte crucial en el mundo de las finanzas descentralizadas (decentralized finance, DeFi), ofreciendo a sus usuarios control completo sobre sus activos y, simultáneamente, provee una plataforma para las transacciones de criptomonedas de confianza. Los lugares de intercambio centralizados tradicionales (traditional centralized exchanges, CEXes) requieren que los usuarios depositen sus activos en la plataforma y renuncien a sus claves privadas, creando un punto de falla centralizado.
¿Estás interesado en crear tu propio lugar de intercambio descentralizado en la blockchain de Ethereum? ¡No mires más! Este artículo te guiará a través del proceso, proveyendo ejemplos de código en Solidity y configurando un entorno de desarrollo usando HardHat.
¿Funcionalidad del Intercambio Descentralizado?
Un intercambio descentralizado básico debería permitir a los usuarios a:
- Depositar tokens o ether.
- Intercambiar tokens o ether en una forma de confianza.
- Retirar tokens o ether.
Configurando un entorno de Desarrollo
Primero, configuremos nuestro entorno usando HardHat. Sigue estos simples pasos:
- Instala Node.js si no lo has hecho aún. Puedes descargarlo aquí
- Crea un nuevo directorio para tu proyecto y navega en el en tu terminal o el prompt de tu comando
- En el directorio del proyecto, ejecuta el comando
npm init
para iniciar un nuevo proyecto Node.js - Instala HardHat ejecutando
npm install --save-dev hardhat
- Ejecuta
npx hardhat
para generar una muestra de archivo de configuración HardHat y elige crear una prueba de muestra cuando se solicite - Borra los contratos de muestra (
Greeter.sol
yToken.sol
) así como el archivo de prueba (sample-test.js
)
Ahora, tienes un proyecto configurado en blanco de HardHat y estás listo para empezar a escribir tu propio intercambio descentralizado desde cero.
Crear un Token para Nuestro Intercambio
Primero, crearemos un token ERC20 para nuestra demostración. Un token ERC20 es un token estándar totalmente aceptable en la blockchain de Ethereum, permitiendo una interoperabilidad sin problemas entre diferentes aplicaciones descentralizadas.
En el directorio de tu proyecto, crea un nuevo directorio llamado contracts
. Dentro del directorio contracts
, crea un nuevo archivo llamado ´MyToken.sol´. El código de nuestro código sencillo ERC20 es como sigue:
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
constructor(uint256 initialSupply) ERC20("MyToken", "MTK") {
_mint(msg.sender, initialSupply);
}
}
Aquí, podemos hacer uso del contrato ERC20
proveído por la biblioteca OpenZeppelin. Se encarga de los detalles de la implementación del token ERC20, así que podemos simplemente proveer un suministro inicial y el nombre y símbolo de nuestro token.
Construir un Contrato de Intercambio Descentralizado
Ahora, crea un nuevo archivo llamado DEX.sol
en el directorio contracts
. Aquí es donde desarrollaremos nuestro contrato inteligente para el intercambio descentralizado.
Inicialización
Comencemos definiendo la estructura básica de nuestro contrato y inicializarlo:
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract DEX {
using SafeERC20 for ERC20; // Use OpenZeppelin's SafeERC20 to deal with tokens securely
address public owner; // The owner of the DEX
constructor() {
owner = msg.sender;
}
}
Aquí, importaremos ambos: los contratos ERC20
y SafeERC20
desde la biblioteca OpenZeppelin. También configuraremos el dueño del DEX a la dirección que despliega el contrato.
Añadiendo soporte al Token
Ahora, construiremos la funcionalidad para añadir nuevos tokens a nuestro DEX:
struct Token {
ERC20 tokenContract;
uint256 totalLiquidity;
}
mapping(address => Token) public tokens; // Address of token contract to Token struct
function addToken(address tokenAddress) public {
require(msg.sender == owner, "Only owner can add tokens.");
ERC20 token = ERC20(tokenAddress);
tokens[tokenAddress] = Token({tokenContract: token, totalLiquidity: 0});
}
Podemos definir una nueva estructura Token
, el cual mantiene una instancia del contrato del token y la liquidez total del token en nuestro DEX. Crearemos un mapping desde la dirección del contrato del token a la estructura del Token
. Luego, añadiremos la función addToken
que permite al dueño del DEX añadir algunos nuevos tokens proveyendo la dirección del contrato del token.
Date cuenta que la declaración require
para asegurar que sólo el dueño pueda añadir tokens: revisa si la función de llamada es el dueño y, si no lo es, la transacción es revertida con un mensaje de error personalizado.
Funciones de Depósito y Retiro
Luego, implementaremos las funciones de deposit
y withdraw
. Los usuarios deberían ser capaces de depositar y retirar tokens así como ether. Para los depósitos y retiradas de token, usaremos las funciones de transfer
y transferFrom
, proveídas por el contrato ERC20
.
Crea las siguientes funciones en el archivo DEX.sol
:
mapping(address => mapping(address => uint256)) public tokenBalances; // User address -> token address -> token balance
function depositToken(address tokenAddress, uint256 amount) public {
require(tokenAddress != address(0), "Cannot deposit zero address token."); // Safety check
require(tokens[tokenAddress].tokenContract != ERC20(address(0)), "Token not supported by DEX.");
tokens[tokenAddress].tokenContract.safeTransferFrom(msg.sender, address(this), amount);
tokenBalances[msg.sender][tokenAddress] += amount;
}
function deposit() payable public {
require(msg.value > 0, "Cannot deposit zero value."); // Safety check
tokenBalances[msg.sender][address(0)] += msg.value; // ETH is stored as zero address
}
mapping(address => mapping(address => uint256)) public etherBalances; // User address -> token address -> ether balance
function withdrawToken(address tokenAddress, uint256 amount) public {
require(tokenAddress != address(0), "Cannot withdraw zero address token.");
require(tokens[tokenAddress].tokenContract != ERC20(address(0)), "Token not supported by DEX.");
tokenBalances[msg.sender][tokenAddress] -= amount;
tokens[tokenAddress].tokenContract.safeTransfer(msg.sender, amount);
}
function withdraw(uint256 amount) public {
require(etherBalances[msg.sender][address(0)] >= amount, "Insufficient ether balance.");
etherBalances[msg.sender][address(0)] -= amount;
payable(msg.sender).transfer(amount);
}
Con estas cuatro funciones, los usuarios pueden depositar y retirar ambos tokens y ether, y actualizaremos sus balances apropiadamente.
Intercambio
Ahora, la parte más importante: ¡la funcionalidad de intercambio (trading)!
Primero, crearemos una estructura para un orden:
struct Order {
address trader;
address token;
uint256 tokensTotal;
uint256 tokensLeft;
uint256 etherAmount;
uint256 filled;
}
Luego, definiremos un array para almacenar órdenes abiertas y un mapping para almacenar el historial de almacenamiento. También implementaremos un evento para emitirla cuando una nueva orden se crea:
Order[] public openOrders;
mapping(address => Order[]) public orderHistories;
event OrderPlaced(uint256 orderId, address indexed trader, address indexed token, uint256 tokensTotal, uint256 etherAmount);
Ahora podemos crear la función para colocar una nueva orden:
function placeOrder(address token, uint256 tokensTotal, uint256 etherAmount) public {
require(tokens[token].tokenContract != ERC20(address(0)), "Token not supported by DEX.");
require(tokenBalances[msg.sender][token] >= tokensTotal, "Insufficient token balance.");
Order memory newOrder = Order({
trader: msg.sender,
token: token,
tokensTotal: tokensTotal,
tokensLeft: tokensTotal,
etherAmount: etherAmount,
filled: 0
});
openOrders.push(newOrder);
tokenBalances[msg.sender][token] -= tokensTotal;
emit OrderPlaced(openOrders.length-1, msg.sender, token, tokensTotal, etherAmount);
}
Finalmente implementaremos la función fillOrder
, permitiendo a los usuarios completar órdenes abiertas:
event OrderFilled(uint256 orderId, address indexed trader, uint256 tokensFilled, uint256 etherTransferred);
function fillOrder(uint256 orderId) public {
Order storage order = openOrders[orderId];
uint256 tokensToFill = order.tokensLeft;
uint256 etherToFill = (tokensToFill * order.etherAmount) / order.tokensTotal;
require(etherBalances[msg.sender][address(0)] >= etherToFill, "Insufficient ether balance.");
etherBalances[order.trader][address(0)] += etherToFill;
etherBalances[msg.sender][address(0)] -= etherToFill;
tokenBalances[msg.sender][order.token] += tokensToFill;
order.tokensLeft = 0;
order.filled += tokensToFill;
orderHistories[msg.sender].push(order);
if (order.trader != msg.sender) orderHistories[order.trader].push(order);
delete openOrders[orderId];
emit OrderFilled(orderId, order.trader, tokensToFill, etherToFill);
}
Con la función fill Order
, los usuarios pueden completar las órdenes, intercambiando sus ether por tokens. Luego, la orden es removida desde el listado de órdenes abiertas y luego añadidas al historial de orden de cada grupo.
Prueba y Despliegue
Con nuestro contrato DEX completo, puedes desplegarlo en una blockchain Ethereum local en HardHat, prueba tu código o incluso, despliega en una red de prueba Ethereum.
¡Bien hecho! Ahora haz creado tu propio intercambio descentralizado en la blockchain de Ethereum. Desde aquí, puedes extender la función de tu DEX, añade más intercambios de par o mejora la interfaz del usuario, implementando una aplicación frontend.
Referencias
- Ethereum.org: Solidity
- OpenZeppelin: Biblioteca del Contrato Inteligente
- HardHat:Entorno de Desarrollo Ethereum
El post Cómo Crear Tu Propio Intercambio Descentralizado (DEX) en Solidity y HardHat apareció primero en CriptoLoom.
Este artículo es una traducción de CryptoLoom, hecha por Héctor Botero. Puedes encontrar el artículo original aquí.
Sería genial escucharte en nuestro Discord, puedes contarnos tus ideas, comentarios, sugerencias y dejarnos saber lo que necesitas.
Si prefieres puedes escribirnos a @web3dev_es en Twitter.
Discussion (0)