As exchanges descentralizadas (DEXs) desempenham um papel crucial no mundo das finanças descentralizadas (DeFi), oferecendo aos usuários controle total sobre seus ativos e, ao mesmo tempo, fornecendo uma plataforma para a transação de criptomoedas sem necessidade de confiança em terceiros. As exchanges centralizadas tradicionais (CEXs) exigem que os usuários depositem ativos na plataforma e liberem suas chaves privadas, criando um ponto de falha centralizado.
Você está interessado em criar sua própria exchange descentralizada na blockchain Ethereum? Não procure mais! Este artigo o guiará pelo processo, fornecendo exemplos de código em Solidity e configurando um ambiente de desenvolvimento usando o HardHat.
Funcionalidade da exchange descentralizada
Uma exchange descentralizada básica deve permitir aos usuários:
- Depositar tokens ou ether.
- Negociar tokens ou ether de maneira que não exija confiança em terceiros.
- Sacar tokens ou ether.
Para obter essa funcionalidade, construiremos nossa DEX usando a linguagem de programação de contrato inteligente mais popular da Ethereum, o Solidity. Para tornar nosso processo de desenvolvimento simplificado e eficiente, usaremos o ambiente de desenvolvimento do HardHat.
Configurando o ambiente de desenvolvimento
Primeiro, vamos configurar nosso ambiente de desenvolvimento usando o HardHat. Siga estas etapas simples:
- Instale o Node.js caso ainda não o tenha feito. Você pode baixá-lo aqui.
- Crie um novo diretório para seu projeto e navegue até ele em seu terminal ou prompt de comando.
- No diretório do projeto, execute o comando
npm init
para inicializar um novo projeto Node.js. - Instale o HardHat executando
npm install --save-dev hardhat
. - Execute
npx hardhat
para gerar um arquivo de configuração de amostra do HardHat e escolha criar um projeto de amostra quando o prompt for executado. - Exclua os contratos de amostra (
Greeter.sol
eToken.sol
), bem como o arquivo de teste (sample-test.js
).
Agora, você tem um projeto HardHat em branco configurado e está pronto para começar a desenvolver sua própria exchange descentralizada do zero.
Criando um token para nossa exchange
Primeiro, criaremos um token ERC20 para nossa demonstração. Um token ERC20 é um padrão de token amplamente aceito na blockchain Ethereum, permitindo a interoperabilidade perfeita entre diferentes aplicativos descentralizados.
No diretório do seu projeto, crie um novo diretório chamado contracts
. Dentro do diretório contracts
, crie um novo arquivo chamado MyToken.sol
. O código para nosso token ERC20 simples é o seguinte:
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);
}
}
Aqui, usamos o contrato ERC20
fornecido pela biblioteca OpenZeppelin. Ela cuida dos detalhes da implementação do token ERC20, para que possamos simplesmente fornecer um suprimento inicial, o nome e o símbolo do nosso token.
Construindo o contrato da exchange descentralizada
Em seguida, crie um novo arquivo chamado DEX.sol
no diretório contracts
. É nele que desenvolveremos o contrato inteligente da nossa exchange descentralizada.
Inicialização
Vamos começar definindo a estrutura básica do nosso contrato e inicializando-o:
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 o SafeERC20 do OpenZeppelin para lidar com tokens de forma segura
address public owner; // O proprietário da DEX
constructor() {
owner = msg.sender;
}
}
Aqui, importamos os contratos ERC20
e SafeERC20
da biblioteca OpenZeppelin. Também definimos o proprietário da DEX para o endereço que implanta o contrato.
Adicionando Suporte a Tokens
Agora, vamos construir a funcionalidade para adicionar novos tokens à nossa DEX:
struct Token {
ERC20 tokenContract;
uint256 totalLiquidity;
}
mapping(address => Token) public tokens; // Endereço do contrato de token para a estrutura do token
function addToken(address tokenAddress) public {
require(msg.sender == owner, "Somente o proprietario pode adicionar tokens.");
ERC20 token = ERC20(tokenAddress);
tokens[tokenAddress] = Token({tokenContract: token, totalLiquidity: 0});
}
Definimos uma nova struct Token
, que contém uma instância do contrato do token e a liquidez total do token em nossa DEX. Criamos um mapeamento do endereço do contrato do token para a struct Token
. Em seguida, adicionamos uma função addToken
que permite ao proprietário da DEX adicionar novos tokens fornecendo o endereço do contrato do token.
Observe a declaração require
para garantir que apenas o proprietário possa adicionar tokens: ela verifica se o chamador da função é o proprietário e, caso não seja, a transação é revertida com uma mensagem de erro personalizada.
Funções de depósito e saque
A seguir, implementaremos as funções deposit
(depósito) e withdraw
(saque). Os usuários devem poder depositar e sacar tokens, bem como ether. Para depósitos e saques de tokens, usaremos as funções transfer
e transferFrom
fornecidas pelo contrato ERC20
.
Crie as seguintes funções no arquivo DEX.sol
:
mapping(address => mapping(address => uint256)) public tokenBalances; // Endereço do usuário -> endereço do token -> saldo do token
function depositToken(address tokenAddress, uint256 amount) public {
require(tokenAddress != address(0), "Não é possível depositar token de endereço zero."); // Verificação de segurança
require(tokens[tokenAddress].tokenContract != ERC20(address(0)), "Token não suportado pela DEX.");
tokens[tokenAddress].tokenContract.safeTransferFrom(msg.sender, address(this), amount);
tokenBalances[msg.sender][tokenAddress] += amount;
}
function deposit() payable public {
require(msg.value > 0, "Não é possível depositar um valor zero."); // Verificação de segurança
tokenBalances[msg.sender][address(0)] += msg.value; // O ETH é armazenado como endereço zero
}
mapping(address => mapping(address => uint256)) public etherBalances; // Endereço do usuário -> endereço do token -> saldo de ether
function withdrawToken(address tokenAddress, uint256 amount) public {
require(tokenAddress != address(0), "Não é possível retirar token de endereço zero.");
require(tokens[tokenAddress].tokenContract != ERC20(address(0)), "Token não suportado pela 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, "Balanço insuficiente de ether.");
etherBalances[msg.sender][address(0)] -= amount;
payable(msg.sender).transfer(amount);
}
Com essas quatro funções, os usuários podem depositar e sacar tokens e ether e nós atualizamos seus saldos de acordo.
Negociação
Agora a parte mais importante: a funcionalidade de negociação!
Primeiro, crie uma struct para um pedido:
struct Order {
address trader;
address token;
uint256 tokensTotal;
uint256 tokensLeft;
uint256 etherAmount;
uint256 filled;
}
Em seguida, defina um array para armazenar pedidos em aberto e um mapeamento para armazenar o histórico de pedidos. Também implementamos um evento a ser emitido quando um novo pedido é criado:
Order[] public openOrders;
mapping(address => Order[]) public orderHistories;
event OrderPlaced(uint256 orderId, address indexed trader, address indexed token, uint256 tokensTotal, uint256 etherAmount);
Agora podemos criar a função para fazer um novo pedido:
function placeOrder(address token, uint256 tokensTotal, uint256 etherAmount) public {
require(tokens[token].tokenContract != ERC20(address(0)), "Token não suportado pela DEX.");
require(tokenBalances[msg.sender][token] >= tokensTotal, "Saldo insuficiente de tokens.");
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);
}
Por fim, vamos implementar a função fillOrder
, permitindo que os usuários concluam pedidos em aberto:
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, "Saldo insuficiente de ether.");
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);
}
Com a função fillOrder
, os usuários podem concluir pedidos, trocando seu ether por tokens. O pedido é removido da lista de pedidos em aberto e adicionado ao histórico de pedidos de cada parte.
Teste e implantação
Com nosso contrato DEX concluído, agora você pode implantá-lo em uma blockchain Ethereum local usando o HardHat, testar seu código ou até mesmo implantá-lo em uma rede pública de teste Ethereum.
Bom trabalho! Agora você criou sua própria exchange descentralizada na blockchain Ethereum. A partir daqui, você pode estender a funcionalidade da sua DEX, adicionar mais pares de negociação ou melhorar a interface do usuário implementando um aplicativo front-end.
Referências
- Ethereum.org: Solidity
- OpenZeppelin: Biblioteca de contratos inteligentes
- HardHat: Ambiente de Desenvolvimento Ethereum
O post Como criar sua própria exchange descentralizada (DEX) em Solidity e HardHat apareceu primeiro em CryptoLoom.
Artigo escrito por CryptoLoom. Traduzido por Marcelo Panegali.
Top comments (0)