👩💻先决条件
您将需要安装NodeJS>=10、Yarn和Git。
本教程假设您对Web 应用程序开发有基本的了解,并且对以太坊的核心概念有所了解。
🙇♀️ 开始
在 🛠 Programming Decentralized Money中,我们介绍了 🏗 scaffold-eth。本教程将使用dex以太坊开发脚手架的分支:
git 克隆https://github.com/austintgriffith/scaffold-eth.git dex
cd dex
git 结账 dex
纱线安装
纱线开始
(更新回购:https ://github.com/scaffold-eth/scaffold-eth-challenges/tree/challenge-4-dex )
我们还想启动我们的本地区块链并部署我们的合约。在新的终端窗口中,运行:
纱线运行链
在第三个终端窗口中,我们可以编译和部署:
纱线运行部署
该应用程序应该出现在http://localhost:3000并且您应该看到:
☢️。如果这不是您看到的标头,则您可能在错误的 git 分支上。
我们还将看到两个智能合约显示为DEX和Balloons。
我们可以在以下位置找到这些智能合约packages/buidler/contracts:
Balloons.sol只是一个 ERC20 合约示例,它向部署它的任何地址铸造 1000。
DEX.sol是我们将在本教程中构建的内容,您可以看到它以一个SafeMath库开始,以帮助我们防止上溢和下溢,并跟踪token我们在构造函数中(部署时)设置的(ERC20 接口):
☢️ 你会在 中找到智能合约packages/buidler/contracts。还有其他contracts文件夹,因此请确保找到正确的文件夹DEX.sol。
💰储备
如介绍中所述,我们希望创建一个自动市场,我们的合约将持有和的储备金。这些储备将提供允许任何人在资产之间进行交换。让我们添加几个新变量到:ETH🎈BalloonsliquidityDEX.sol
uint256 public totalLiquidity;
映射(地址 => uint256)公共流动性;
这些变量跟踪总流动性,但也跟踪单个地址。
然后,让我们创建一个init()函数DEX.sol,payable然后我们可以定义tokens它将转移给自己的数量:
function init(uint256 tokens) public payable returns (uint256) {
require(totalLiquidity==0,"DEX:init - already has liquidity");
totalLiquidity = address(this).balance;
流动性[msg.sender] = totalLiquidity;
要求(token.transferFrom(msg.sender,address(this),tokens));
返回总流动性;
}
调用init()将使我们的合约加载ETH和🎈Balloons。
您可以使用命令编译您的合同,yarn run compile现在,只需忽略任何警告。准备就绪后,部署您的合约:
纱线运行部署
您的应用程序应该热重载并且 DEX 合约应该位于新地址。另外,我们的新流动性变量会自动加载到前端:
我们可以看到 DEX 一开始是空的。我们希望能够以init()流动性开始,但我们还没有任何资金或代币。
🏗 Scaffold-eth 在页面加载时使用一个临时帐户启动每个用户。让我们在右上角复制我们的地址:
并从左下方的水龙头向我们的帐户发送一些测试 ETH:
现在我们需要一些🎈气球代币!在中找到该deploy.js文件,packages/buidler/scripts让我们添加一行,在部署合约时向我们的帐户发送 10 个令牌(10 乘以 10¹⁸,因为没有小数):
现在重新部署一切,我们将看到新的地址合约,但更重要的是,我们应该在部署时向我们发送 10 个令牌:
纱线运行部署
要在前端查看我们的🎈 气球,您会看到我们正在使用App.js. (您可以App.js在packages/react-app/src目录中找到。)
我们还不能打电话init(),因为 DEX 合约不允许从我们的账户转移代币。我们需要approve()与DEXBalloons UI 签订合同。复制并粘贴DEX地址,然后将数量设置为5000000000000000000(5 * 10¹⁸):
如果您点击该💸图标,它应该会触发一个交易,批准DEX您拿走您的 5 个代币。您可以使用以下allowance表格对此进行测试:
现在我们准备调用init(). DEX我们将告诉它拿走 5 (10¹⁸) 的代币,我们还将通过交易发送 0.01 ETH。您可以通过输入交易价值0.01来完成此操作,然后点击✳️ 执行 10¹⁸,然后点击#️⃣ 将其转换为十六进制:
一旦你按下💸按钮,你的交易将发送 ETH,合约将获取你的 5 个代币:
您可以使用 UI 检查 DEX 有多少个🎈气球:
这工作得很好,但如果我们在init()部署合约时只调用该函数,将会容易得多。在deploy.js脚本中尝试取消对 init 部分的注释,这样我们DEX将从 5ETH和 5Balloons的流动性开始:
现在我们yarn run deploy的合约应该在部署后立即初始化,我们应该拥有相等的 ETH 和代币储备。
📉 价格
现在我们的合约同时持有ETH 和代币储备,我们想使用一个简单的公式来确定两者之间的汇率。
x * y = k让我们从储备的公式开始x:y
(DEX 中的 ETH 数量)*(DEX 中的代币数量)= k
之所以称为不变量,是因为它在交易期间k不会改变。(唯一的变化是增加了流动性。)如果我们绘制这个公式,我们将得到一条看起来像这样的曲线:k
💡 我们只是将一种资产换成另一种资产,“价格”基本上是如果您投入一定数量的输入资产,您将获得多少最终输出资产。
🤔哦!基于像这样的曲线的市场将始终具有流动性,但是随着比率变得越来越不平衡,您从相同的交易量中获得的较弱资产会越来越少。同样,如果智能合约有太多的 ETH 而没有足够的代币,则将代币兑换为 ETH 的价格应该更可取。
当我们调用时,我们以 1:1 的比例init()传入 ETH 和代币,并且该比例必须保持不变。当一种资产的储备发生变化时,另一种资产也必须反向变化。
让我们编辑我们的DEX.sol智能合约并引入这个价格函数:
函数价格(uint256 input_amount,uint256 input_reserve,uint256 output_reserve)公共视图返回(uint256){
uint256 input_amount_with_fee = input_amount.mul(997);
uint256 分子 = input_amount_with_fee.mul(output_reserve);
uint256 分母 = input_reserve.mul(1000).add(input_amount_with_fee);
返回分子/分母;
}
我们使用投入储备与产出储备的比率来计算将其中一种资产换成另一种资产的价格。让我们部署它并四处看看:
纱线运行部署
假设我们有 100 万个 ETH 和 100 万个代币,如果我们将其放入我们的价格公式中并询问它 1000 个 ETH 的价格,它将几乎是 1:1 的比率:
如果我们投入 1000 ETH,我们将收到 996 个代币。如果我们支付 0.3% 的费用,如果一切都完美的话,它应该是 997。但是,随着我们的合约偏离原始比率,会有一点点下滑。让我们深入了解一下,真正了解这里发生了什么。
假设有 500 万个 ETH 而只有 100 万个代币。然后,我们要放入 1000 个代币。这意味着我们应该收到大约 5000 个 ETH:
最后,假设比率相同,但我们想要交换 100,000 个代币而不是 1000 个。我们会注意到滑点的数量要大得多。我们不会收回 498,000 美元,而只会得到 453,305 美元,因为我们正在减少储备金。
💡 当准备金比率偏离均衡时,合约会自动调整价格。它被称为 🤖自动做市商。
⚖️ 交易
让我们编辑DEX.sol智能合约并添加两个新功能,用于从每个资产交换到另一个:
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);
要求(token.transfer(msg.sender,tokens_bought));
返回 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);
要求(token.transferFrom(msg.sender,address(this),tokens));
返回 eth_bought;
}
这些函数中的每一个都使用我们的价格函数来计算输出资产的最终数量,该价格函数着眼于储备与输入资产的比率。
我们可以打电话tokenToEth,它会拿走我们的代币并向我们发送 ETH,或者我们可以ethToToken在交易中使用一些 ETH 打电话,它会向我们发送代币。
让我们编译并部署我们的合约,然后移至前端:
纱线运行部署
您的应用程序 UI 应该热重载并显示两个新功能:
使用水龙头向我们的账户发送一些 ETH 后,我们可以尝试将一些 ETH 换成代币。让我们开始0.001然后点击✳️然后#️⃣。然后,如果我们点击💸它将使交易调用ethToToken():
我们的账户应该收到 0.001 个代币:
将代币换成 ETH 有点棘手,因为我们必须先与 进行交易approve()才能DEX拿到我们的代币。让我们批准 DEX 地址使用 1 (* 10¹⁸) 个令牌 (1000000000000000000):
然后让我们尝试用 ETH 交换该数量的代币:
那么我们的 ETH 余额应该增加 0.85 左右:
(它以美元显示您的余额,但您可以单击它以查看确切的金额:)
🎉 我们正在交换资产!🎊 用一些表情符号庆祝!🥳🍾🥂
💦 流动性
到目前为止,只有init()功能控制流动性。为了使其更加去中心化,如果任何人都DEX可以通过以正确的比例发送 ETH 和代币来增加流动性池,那就更好了。
让我们创建两个新函数,让我们存入和提取流动性:
function deposit() public payable returns (uint256) {
uint256 eth_reserve = address(this).balance.sub(msg.value);
uint256 token_reserve = token.balanceOf(地址(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);
要求(token.transferFrom(msg.sender,address(this),token_amount));
返回 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);
要求(token.transfer(msg.sender,token_amount));
返回(eth_amount,token_amount);
}
DEX.sol将它们粘贴到in后,花点时间了解这些函数的作用packages/buidler/contracts:
该deposit()函数接收 ETH 并tokens以正确的比例从调用者转移到合约。该合约还跟踪liquidity存款地址拥有的金额与totalLiquidity.
该withdraw()功能允许用户以正确的比例取出 ETH 和代币。由于从每笔交易中收取 0.3% 的费用,流动性提供者提取的 ETH 和代币的实际数量将高于他们存入的数量。这会激励第三方提供流动性。
编译您的合约并将其部署到前端:
纱线运行部署
📲 界面
用户体验非常糟糕/丑陋,仍然很难想象这整个滑点。让我们做一些前端工作来清理界面并使其更易于理解。
让我们编辑App.js:packages/react-app/src
此代码分支中包含一个自定义组件。删除的通用组件DEX并引入类似的组件:
<DEX
address={address}
injectedProvider
={injectedProvider}
localProvider={localProvider} mainnetProvider={mainnetProvider}
readContracts={readContracts}
price={price}
/>
让我们通过给它一个标题并选择只显示balanceOf和approve()动作来清理气球的组件:
<Contract
title={"🎈 Balloons"}
name={"Balloons"}
show={["balanceOf","approve"]}
provider={localProvider}
address={address}
/>
Rad,我们的前端正在寻找 🔥 AF:
🔬探索
现在,用户只需输入他们想要交换的 ETH 或代币的数量,图表就会显示价格的计算方式。用户还可以可视化更大的掉期如何导致更多的滑点和更少的输出资产:
用户还可以从流动性池中存取款,赚取费用:
🎉恭喜!
我们拼凑了一个最小可行的去中心化交易所。我们可以为任何人交换资产提供流动性,流动性提供者将赚取费用。这一切都发生在链上,无法审查或篡改。
这是一个🤖不可阻挡的市场⚖️!!!