Construindo uma exchange descentralizada simplificada na Ethereum
Introdução
Este primeiro aplicativo matador (um programa que é considerado tão necessário e desejável que justifica a compra ou a adoção de um tipo particular de computador) a ser construído na Ethereum foi um token. Um padrão chamado ERC20 surgiu e agora qualquer um pode implantar um token na Ethereum em segundos.
Muitas vezes, um token precisa ser trocado por um valor ou outro token e tradicionalmente isso tem sido facilitado por entidades inseguras e centralizadas.
Em 2017, Vitalik Buterin propôs um método que permitiria que um contrato inteligente realizasse trocas entre dois ativos utilizando um índice de liquidez baseado em reservas. Eventualmente, isto se tornou 🦄Uniswap e agora qualquer um pode usar uniswap.exchange para trocar tokens na Ethereum.
Os contratos Uniswap movimentaram mais de $1,2 BILHÕES de dólares coletivamente. 🤯
Ao longo de 2019, os provedores de liquidez da Uniswap ganharam mais de $1,2 milhões em taxas. — Year in Eth 2019
Neste tutorial, tentaremos reduzir uma troca descentralizada a algumas funções simples de Solidity em um contrato inteligente fácil de digerir. Felizmente, meu amigo Philippe Castonguay
criou uniswap-solidity e este tutorial utilizará uma versão reduzida deste código.
Construiremos uma exchange descentralizada para trocar um token arbitrário por ETH usando um pool de liquidez do qual qualquer pessoa pode participar. Esta construção demonstrará como contratos inteligentes podem criar 🤖 sistemas descentralizados automáticos usando incentivos econômicos em criptos.
🛰 atualização — ago 2022 : Você pode encontrar um repositório atualizado com a última DEX construída aqui:
https://github.com/scaffold-eth/scaffold-eth-challenges/tree/challenge-4-dex
Sumário
1 . Introdução
2 . SpeedRun
4 . Iniciando
5 . Reservas
6 . Preço
7 . Negociação
8 . Liquidez
9 . Interface
10 . Explore
11 . Parabéns
SpeedRun
Assista o vídeo
O que é necessário
Você precisará do NodeJS>=10, Yarn, e Git instalados.
Este tutorial assume que você tem uma compreensão básica de desenvolvimento de app na web e alguma orientação sobre conceitos fundamentais da Ethereum.
Iniciando
Em 🛠 Programando dinheiro descentralizado nós introduzimos 🏗 scaffold-eth. Este tutorial utilizará o ramo dex
do scaffolding de desenvolvimento Ethereum:
git clone https://github.com/austintgriffith/scaffold-eth.git dex
cd dex
git checkout dex
yarn install
yarn start
(repositório ATUALIZADO: https://github.com/scaffold-eth/scaffold-eth-challenges/tree/challenge-4-dex)
Também vamos querer trazer nossa blockchain local e implantar nossos contratos. Em uma nova janela de terminal, executar:
yarn run chain
Em uma terceira janela do terminal, podemos compilar e implantar com:
yarn run deploy
O aplicativo deve chegar em http://localhost:3000 e você deverá ver:
☢️ Se este não é o título que você vê, você provavelmente está no ramo errado.
Também veremos dois contratos inteligentes chamados DEX
e Balloons
.
Podemos encontrar esses contratos inteligentes em packages/buidler/contracts
:
Balloons.sol
é apenas um exemplo de contrato ERC20 que cunha 1000 para qualquer endereço que o implemente.
DEX.sol
é o que vamos construir neste tutorial e você pode ver que ele começa com uma biblioteca SafeMath
para nos ajudar a evitar overflows e underflows e também rastrear um token
(interface ERC20) que definimos no construtor (no deploy):
☢️ Você encontrará os contratos inteligente em packages/buidler/contracts
. Existem outras pastas contracts
então certifique-se de que você vai encontrar a correta com DEX.sol
.
Reservas
Como mencionado na introdução, queremos criar um mercado automático onde nosso contrato manterá reservas de ambos ETH
e 🎈Balloons
. Essas reservas proverão liquidity
que permite a qualquer pessoa fazer trocas entre os ativos. Vamos adicionar algumas variáveis novas a DEX.sol
:
uint256 public totalLiquidity;
mapping (address => uint256) public liquidity;
Essas variáveis rastreiam a liquidez total, mas também por endereços individuais.
Então, vamos criar uma função init()
em DEX.sol
que é payable
e então podemos definir uma quantidade de tokens
que vai transferir para si mesma:
function init(uint256 tokens) public payable returns (uint256) {
require(totalLiquidity==0,"DEX:init - already has liquidity");
totalLiquidity = address(this).balance;
liquidity[msg.sender] = totalLiquidity;
require(token.transferFrom(msg.sender, address(this), tokens));
return totalLiquidity;
}
Chamando init()
nosso contrato será carregado com ambos ETH
e 🎈Balloons
.
Você pode compilar seus contratos com o comando yarn run compile
e por enquanto, basta ignorar quaisquer avisos. Quando você estiver pronto, implante seus contratos:
yarn run deploy
Seu aplicativo deve recarregar automaticamente e o contrato DEX deve estar em um novo endereço. Além disso, nossas novas variáveis de liquidez são automaticamente carregadas no frontend:
Podemos ver que o DEX começa vazio. Queremos ser capazes de chamar init()
para começar com liquidez, mas ainda não temos fundos ou tokens.
🏗 Scaffold-eth inicia cada usuário com uma conta temporária no carregamento da página. Vamos copiar nosso endereço na parte superior direita:
E envie para nossa conta algum ETH de teste da faucet na parte inferior esquerda:
Agora precisamos de alguns tokens 🎈Balloon! Encontre o arquivo deploy.js
em packages/buidler/scripts
e vamos adicionar uma linha que envie 10 tokens (10 vezes 10¹⁸ porque não há decimais) para nossa conta quando o contrato for implantado:
Agora, reimplante tudo e veremos novos contratos de endereços, mas, mais importante, devemos ter 10 tokens enviados para nós no momento da implantação:
yarn run deploy
Para ver nossos 🎈balloons no frontend você verá que estamos usando o componente <TokenBalance>
logo abaixo do componente <Account>
em App.js
. (Você pode encontrar App.js
no diretório packages/react-app/src
.)
Ainda não podemos simplesmente chamar o init()
porque o contrato DEX não permite a transferência de tokens da nossa conta. Precisamos approve()
o contrato DEX
com a UI Balloons. Copie e cole o endereço DEX
e então defina o amount para 5000000000000000000
(5 * 10¹⁸):
Se você pressionar o ícone 💸, deverá disparar uma transação que aprova o DEX para tirar 5 de seus tokens. Você pode testar isso com o formulário allowance
:
Agora estamos prontos para chamar o init()
na DEX
. Diremos a ela para levar 5 (*10¹⁸) de nossos tokens e também enviaremos 0,01 ETH com a transação. Você pode fazer isso digitando 0.01
como o valor da transação, então pressione ✳️ para fazer *10¹⁸, depois pressione #️⃣ para convertê-lo em hex:
Uma vez que você pressione o botão 💸
sua transação enviará em ETH e o contrato pegará 5 de seus tokens:
Você pode verificar quantos balloons 🎈a DEX tem, usando a UI:
Isto funciona muito bem, mas será muito mais fácil se apenas chamarmos a função init()
enquanto implantamos o contrato. No script deploy.js
tente descomentar a seção init de forma que a nossa DEX
começará com 5 ETH
e 5 Balloons
de liquidez:
Agora, quando nós yarn run deploy,
nosso contrato deve ser inicializado assim que for executado e devemos ter reservas iguais de ETH e tokens.
Preço
Agora que nosso contrato mantém reservas tanto de ETH quanto de tokens, queremos usar uma fórmula simples para determinar a taxa de câmbio entre os dois.
Vamos começar com a fórmula x * y = k
onde x
e y
são as reservas:
( amount of ETH in DEX ) * ( amount of tokens in DEX ) = k
O k
é chamado de constante porque não muda durante as negociações. (O k
só muda quando _liquidity _é adicionada.) Se plotarmos esta fórmula, teremos uma curva que parece algo como:
💡 Estamos apenas trocando um ativo por outro, o "preço" é basicamente quanto do ativo de saída resultante você receberá se você colocar uma certa quantidade do ativo de entrada.
🤔 OH! Um mercado baseado em uma curva como esta sempre terá liquidez, mas à medida que a relação se torna cada vez mais desequilibrada, você receberá cada vez menos do ativo mais fraco do mesmo valor de negociação. Mais uma vez, se o contrato inteligente tiver muito ETH e não houver tokens suficientes, o preço para trocar tokens por ETH deverá ser mais desejável.
Quando chamamos init()
passamos em ETH e tokens a uma proporção de 1:1 e essa proporção deve permanecer constante. Como as reservas de um ativo mudam, o outro ativo também deve mudar inversamente.
Vamos editar nosso contrato inteligente DEX.sol
e trazer esta função price:
function price(uint256 input_amount, uint256 input_reserve, uint256 output_reserve) public view returns (uint256) {
uint256 input_amount_with_fee = input_amount.mul(997);
uint256 numerator = input_amount_with_fee.mul(output_reserve);
uint256 denominator = input_reserve.mul(1000).add(input_amount_with_fee);
return numerator / denominator;
}
Usamos a razão entre a reserva de input e a reserva de output para calcular o preço de troca de um ativo pelo outro. Vamos implantar isto e dar uma olhada:
yarn run deploy
Digamos que temos 1 milhão de ETH e 1 milhão de tokens. Se colocarmos isso em nossa fórmula de preço e perguntarmos o preço de 1000 ETH, será uma proporção de quase 1:1:
Se pusermos 1000 ETH, receberemos 996 tokens. Se estivermos pagando uma taxa de 0,3%, deveria ser 997 se tudo estivesse perfeito. MAS, há um pequena derrapagem à medida que nosso contrato se afasta da proporção original. Vamos aprofundar mais para entender realmente o que está acontecendo aqui.
Digamos que há 5 milhões de ETH e apenas 1 milhão de tokens. Então, queremos colocar 1.000 tokens. Isso significa que devemos receber cerca de 5.000 ETH:
Finalmente, vamos dizer que a proporção é a mesma, mas queremos trocar 100.000 tokens em vez de apenas 1000. Vamos notar que a quantidade de derrapagem é muito maior. Em vez de 498.000 de volta, receberemos apenas 453.305 porque estamos fazendo um grande furo nas reservas.
💡 O contrato ajusta automaticamente o preço à medida que a relação de reservas se afasta do equilíbrio. É chamado de um 🤖 Formador de mercado automatizado.
Negociação
Vamos editar o contrato inteligente DEX.sol
e adicionar duas funções novas para trocar cada ativo pelo outro:
function ethToToken() public payable returns (uint256) {
uint256 token_reserve = token.balanceOf(address(this));
uint256 tokens_bought = price(msg.value, address(this).balance.sub(msg.value), token_reserve);
require(token.transfer(msg.sender, tokens_bought));
return tokens_bought;
}
function tokenToEth(uint256 tokens) public returns (uint256) {
uint256 token_reserve = token.balanceOf(address(this));
uint256 eth_bought = price(tokens, token_reserve, address(this).balance);
msg.sender.transfer(eth_bought);
require(token.transferFrom(msg.sender, address(this), tokens));
return eth_bought;
}
Cada uma dessas funções calcula a quantidade resultante do ativo de output usando nossa função de preço que analisa a relação entre as reservas e o ativo de input.
Podemos chamar tokenToEth
e ela tomará nossos tokens e nos enviará ETH ou podemos chamar ethToToken
com algum ETH na transação e ela nos enviará tokens.
Vamos compilar e implantar nosso contrato e depois passar para o frontend:
yarn run deploy
Sua UI do aplicativo deve fazer recarga automática e mostrar as duas funções novas:
Após enviar algum ETH para nossa conta com a faucet, podemos tentar trocar algum ETH por tokens. Vamos começar com 0.001
e depois clicar em ✳️ e depois #️⃣. Então, se clicarmos em 💸, a transação será feita para chamar ethToToken()
:
E nossa conta deve receber 0,001 tokens de volta:
Trocar tokens por ETH é um pouco mais complicado porque temos que fazer uma transação para approve()
o DEX
para levar nossos tokens primeiro. Vamos aprovar o endereço DEX para levar 1 (* 10¹⁸) tokens (1000000000000000000):
Então vamos tentar trocar essa quantidade de tokens por ETH:
Então nosso saldo ETH deve aumentar 0,85 ou mais:
(Ele mostra seu saldo em USD, mas você pode clicar nele para ver o valor exato:)
🎉 Estamos trocando ativos! 🎊 Celebre com seus emojis! 🥳 🍾 🥂
Liquidez
Até agora, somente a função init()
controla a liquidez. Para tornar isto mais descentralizado, seria melhor se qualquer pessoa contribuisse para o pool de liquidez enviando ao DEX
tanto ETH quanto tokens na proporção correta.
Vamos criar duas novas funções que nos permitem depositar e retirar liquidez:
function deposit() public payable returns (uint256) {
uint256 eth_reserve = address(this).balance.sub(msg.value);
uint256 token_reserve = token.balanceOf(address(this));
uint256 token_amount = (msg.value.mul(token_reserve) / eth_reserve).add(1);
uint256 liquidity_minted = msg.value.mul(totalLiquidity) / eth_reserve;
liquidity[msg.sender] = liquidity[msg.sender].add(liquidity_minted);
totalLiquidity = totalLiquidity.add(liquidity_minted);
require(token.transferFrom(msg.sender, address(this), token_amount));
return liquidity_minted;
}
function withdraw(uint256 amount) public returns (uint256, uint256) {
uint256 token_reserve = token.balanceOf(address(this));
uint256 eth_amount = amount.mul(address(this).balance) / totalLiquidity;
uint256 token_amount = amount.mul(token_reserve) / totalLiquidity;
liquidity[msg.sender] = liquidity[msg.sender].sub(eth_amount);
totalLiquidity = totalLiquidity.sub(eth_amount);
msg.sender.transfer(eth_amount);
require(token.transfer(msg.sender, token_amount));
return (eth_amount, token_amount);
}
Tire um segundo para entender o que essas funções estão fazendo, depois de colá-las no DEX.sol
em packages/buidler/contracts
:
A função deposit()
recebe ETH e também transfere tokens
do chamador para o contrato na proporção certa. O contrato também rastreia o valor da liquidity
que o endereço do depositante possui vs totalLiquidity
.
A função withdraw()
permite que um usuário tire tanto ETH quanto tokens na proporção correta. A quantidade real de ETH e tokens que um provedor de liquidez retira será maior do que o que eles depositaram, por causa das taxas de 0,3% cobradas de cada negociação. Isto incentiva terceiros a fornecer liquidez.
Compile e implante seus contratos para o frontend:
yarn run deploy
Interface
A UX é muito ruim/feia e ainda é um pouco difícil visualizar toda esta derrapagem. Vamos fazer algum trabalho de frontend para limpar a interface e torná-la mais fácil de entender.
Vamos editar App.js
em packages/react-app/src
:
Há um componente personalizado <DEX>
incluído neste ramo de código. Exclua o componente genérico <Contract>
para o DEX
e traga o <DEX>
como:
<DEX
address={address}
injectedProvider={injectedProvider}
localProvider={localProvider}
mainnetProvider={mainnetProvider}
readContracts={readContracts}
price={price}
/>
Vamos limpar o componente <Contract>
do Balloon dando a ele um título e optando por mostrar apenas as ações balanceOf
e approve()
:
<Contract
title={"🎈 Balloons"}
name={"Balloons"}
show={["balanceOf","approve"]}
provider={localProvider}
address={address}
/>
Fantástico, nosso frontend está parecendo 🔥 AF:
Explore
Agora, um usuário pode simplesmente digitar a quantidade de ETH ou tokens que deseja trocar e o gráfico mostrará como o preço é calculado. O usuário também pode visualizar como trocas maiores resultam em mais derrapagens e menos ativos de saída:
Um usuário também pode depositar e retirar do pool de liquidez, ganhando taxas:
Parabéns
Juntos, remendamos uma exchange descentralizada viável mínima. Podemos fornecer liquidez para que qualquer pessoa possa trocar ativos e os provedores de liquidez ganharão taxas. Tudo isso acontece on-chain e não pode ser censurado nem adulterado.
É 🤖 um mercado incontrolável** **⚖️!!!
Fale comigo pelo DM no Twitter: @austingriffith
Aqui está nosso contrato de exchange final em toda sua glória:
Agradecimentos a Cooper Turley e Veronica Zhixing Zheng
Este artigo foi escrito por Austin Thomas Griffith e traduzido por Fátima Lima. O original pode ser lido aqui.
Oldest comments (0)