Web3系列教程之入门篇---13. 像 Uniswap 一样建立自己的去中心化交易所
现在是时候为你的Crypto Dev
代币启动 DeFi 交易所了
要求
- 仅使用一个资产对(Eth / Crypto Dev)建立交易所
- 您的去中心化交易所应收取掉1%费用
- 当用户增加流动性时,应该给他们Crypto Dev LP代币(流动性提供者代币)
- 应根据Ether用户愿意增加流动性的比例给予 CD LP 代币
让我们开始开发🚀
先决条件
- 您已完成ICO 教程
- 您已完成Defi 交换理论教程
- 您已完成混合主题教程
智能合约
为了构建智能合约,我们将使用Hardhat。Hardhat 是一个以太坊开发环境和框架,专为 Solidity 中的全栈开发而设计。简单来说,您可以编写智能合约、部署它们、运行测试和调试代码。
- 要设置安全帽项目,请打开终端并执行以下命令
|
|
- 在安装 Hardhat 的同一目录中运行:
|
|
-
选择
Create a Javascript project
-
按回车键已指定
Hardhat Project root
-
如果您想添加一个问题,请按 Enter 键
.gitignore
-
按回车键
Do you want to install this sample project's dependencies with npm (@nomicfoundation/hardhat-toolbox)?
-
现在你有一个安全帽项目准备好了!
-
如果您在 Windows 上,请执行此额外步骤并安装这些库:)
|
|
- 现在在同一个终端中安装
@openzeppelin/contracts
,因为我们将在我们的合同中导入Openzeppelin 的 ERC20Exchange
合同
|
|
-
在目录中创建一个
contracts
名为Exchange.sol
. 在本教程中,我们将分别介绍合同的每个部分-
首先让我们从导入开始
ERC20.sol
- 我们导入
ERC20.sol
是因为我们的交易所需要铸造和创建Crypto Dev LP
代币,这就是它需要继承 ERC20.sol 的原因
- 我们导入
-
|
|
- `constructor`现在让我们为我们的合约创建一个
- 它将`_CryptoDevToken`您在教程中部署的地址`ICO`作为输入参数
- 然后检查地址是否为空地址
- 在所有检查之后,它将值分配给`cryptoDevTokenAddress`变量的输入参数
- 构造函数还为我们的令牌设置`name`and`symbol``Crypto Dev LP`
|
|
-
是时候创建一个函数来获取合约持有的
Eth
和代币的储备了。Crypto Dev
- 众所周知,Eth 储备金将等于合约的余额,并且可以使用
address(this).balance
因此我们创建一个仅用于获取Crypto Dev
代币储备金的函数 - 我们知道
Crypto Dev Token
我们部署的合约是 ERC20 - 所以我们可以调用
balanceOf
来检查CryptoDev Tokens
合约address
持有的余额
- 众所周知,Eth 储备金将等于合约的余额,并且可以使用
|
|
-
是时候创建一个以合约形式
addLiquidity
添加的函数了liquidity``Ether``Crypto Dev tokens
-
如果
cryptoDevTokenReserve
为零,则表示这是第一次有人向合约添加Crypto Dev
代币Ether
-
当用户添加初始流动性时,我们不必保持任何比率,因为我们没有任何流动性。因此,我们接受用户在初始调用中发送的任何数量的令牌
-
如果
cryptoDevTokenReserve
不是零,那么我们必须确保当有人增加流动性时,它不会影响市场当前的价格 -
为了确保这一点,我们保持一个必须保持不变的比率
-
比率是
(cryptoDevTokenAmount user can add/cryptoDevTokenReserve in the contract) = (Eth Sent by the user/Eth Reserve in the contract)
-
Crypto Dev
这个比率决定了在给定一定数量的 Eth 的情况下用户可以提供多少代币 -
当用户增加流动性时,我们需要为他提供一些
LP
代币,因为我们需要跟踪他提供给合约的流动性数量 -
LP
铸造给用户的代币数量与用户Eth
提供的代币数量成比例 -
在初始流动性情况下,当没有流动性时:
LP
将铸造给用户的代币数量等于Eth
合约的余额(因为余额等于Eth
用户在addLiquidity
调用中发送的) -
当合约中已经存在流动性时,铸造的
LP
代币数量基于一个比率。 -
比率是
(LP tokens to be sent to the user (liquidity) / totalSupply of LP tokens in contract) = (Eth sent by the user) / (Eth reserve in the contract)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
/** * @dev Adds liquidity to the exchange. */ function addLiquidity(uint _amount) public payable returns (uint) { uint liquidity; uint ethBalance = address(this).balance; uint cryptoDevTokenReserve = getReserve(); ERC20 cryptoDevToken = ERC20(cryptoDevTokenAddress); /* If the reserve is empty, intake any user supplied value for `Ether` and `Crypto Dev` tokens because there is no ratio currently */ if(cryptoDevTokenReserve == 0) { // Transfer the `cryptoDevToken` from the user's account to the contract cryptoDevToken.transferFrom(msg.sender, address(this), _amount); // Take the current ethBalance and mint `ethBalance` amount of LP tokens to the user. // `liquidity` provided is equal to `ethBalance` because this is the first time user // is adding `Eth` to the contract, so whatever `Eth` contract has is equal to the one supplied // by the user in the current `addLiquidity` call // `liquidity` tokens that need to be minted to the user on `addLiquidity` call should always be proportional // to the Eth specified by the user liquidity = ethBalance; _mint(msg.sender, liquidity); // _mint is ERC20.sol smart contract function to mint ERC20 tokens } else { /* If the reserve is not empty, intake any user supplied value for `Ether` and determine according to the ratio how many `Crypto Dev` tokens need to be supplied to prevent any large price impacts because of the additional liquidity */ // EthReserve should be the current ethBalance subtracted by the value of ether sent by the user // in the current `addLiquidity` call uint ethReserve = ethBalance - msg.value; // Ratio should always be maintained so that there are no major price impacts when adding liquidity // Ratio here is -> (cryptoDevTokenAmount user can add/cryptoDevTokenReserve in the contract) = (Eth Sent by the user/Eth Reserve in the contract); // So doing some maths, (cryptoDevTokenAmount user can add) = (Eth Sent by the user * cryptoDevTokenReserve /Eth Reserve); uint cryptoDevTokenAmount = (msg.value * cryptoDevTokenReserve)/(ethReserve); require(_amount >= cryptoDevTokenAmount, "Amount of tokens sent is less than the minimum tokens required"); // transfer only (cryptoDevTokenAmount user can add) amount of `Crypto Dev tokens` from users account // to the contract cryptoDevToken.transferFrom(msg.sender, address(this), cryptoDevTokenAmount); // The amount of LP tokens that would be sent to the user should be propotional to the liquidity of // ether added by the user // Ratio here to be maintained is -> // (LP tokens to be sent to the user (liquidity)/ totalSupply of LP tokens in contract) = (Eth sent by the user)/(Eth reserve in the contract) // by some maths -> liquidity = (totalSupply of LP tokens in contract * (Eth sent by the user))/(Eth reserve in the contract) liquidity = (totalSupply() * msg.value)/ ethReserve; _mint(msg.sender, liquidity); } return liquidity; }
-
-
现在让我们
removing liquidity
从合约中创建一个函数。- 将发送回用户的以太币数量将基于一个比率
- 比率是
(Eth sent back to the user) / (current Eth reserve) = (amount of LP tokens that user wants to withdraw) / (total supply of LP tokens)
- 将被发送回用户的 Crypto Dev 代币的数量也将基于一个比率
- 比率是
(Crypto Dev sent back to the user) / (current Crypto Dev token reserve) = (amount of LP tokens that user wants to withdraw) / (total supply of LP tokens)
LP
用户用于消除流动性的代币数量将被烧毁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
/** * @dev Returns the amount Eth/Crypto Dev tokens that would be returned to the user * in the swap */ function removeLiquidity(uint _amount) public returns (uint , uint) { require(_amount > 0, "_amount should be greater than zero"); uint ethReserve = address(this).balance; uint _totalSupply = totalSupply(); // The amount of Eth that would be sent back to the user is based // on a ratio // Ratio is -> (Eth sent back to the user) / (current Eth reserve) // = (amount of LP tokens that user wants to withdraw) / (total supply of LP tokens) // Then by some maths -> (Eth sent back to the user) // = (current Eth reserve * amount of LP tokens that user wants to withdraw) / (total supply of LP tokens) uint ethAmount = (ethReserve * _amount)/ _totalSupply; // The amount of Crypto Dev token that would be sent back to the user is based // on a ratio // Ratio is -> (Crypto Dev sent back to the user) / (current Crypto Dev token reserve) // = (amount of LP tokens that user wants to withdraw) / (total supply of LP tokens) // Then by some maths -> (Crypto Dev sent back to the user) // = (current Crypto Dev token reserve * amount of LP tokens that user wants to withdraw) / (total supply of LP tokens) uint cryptoDevTokenAmount = (getReserve() * _amount)/ _totalSupply; // Burn the sent LP tokens from the user's wallet because they are already sent to // remove liquidity _burn(msg.sender, _amount); // Transfer `ethAmount` of Eth from user's wallet to the contract payable(msg.sender).transfer(ethAmount); // Transfer `cryptoDevTokenAmount` of Crypto Dev tokens from the user's wallet to the contract ERC20(cryptoDevTokenAddress).transfer(msg.sender, cryptoDevTokenAmount); return (ethAmount, cryptoDevTokenAmount); }
-
接下来让我们实现交换功能
-
交换有两种方式。一种方法是
Eth
令牌Crypto Dev
,另一种方法Crypto Dev
是Eth
-
重要的是我们要确定:给定用户发送
x
的 / 代币数量Eth
,他将从交换中收到Crypto Dev
多少Eth
/代币?Crypto Dev
-
所以让我们创建一个计算这个的函数:
- 我们会收费
1%
的。这意味着带有费用的输入代币数量将等于Input amount with fees = (input amount - (1*(input amount)/100)) = ((input amount)*99)/100
- 我们需要遵循
XY = K
曲线的概念 - 我们需要确定
(x + Δx) * (y - Δy) = x * y
,所以最终的公式是Δy = (y * Δx) / (x + Δx)
; Δy
在我们的例子中是tokens to be received
,Δx = ((input amount)*99)/100
,x
= 输入储备,y
= 输出储备- 输入储备和输出储备将取决于我们正在实施的交换。
Eth
令牌或Crypto Dev
反之亦然
- 我们会收费
|
|
Ether
现在让我们实现一个函数来交换Crypto Dev
令牌
|
|
- 现在让我们实现一个函数来交换
Crypto Dev
令牌Ether
|
|
- 您的最终合同应如下所示:
|
|
- 现在我们将安装
dotenv
包以便能够导入 env 文件并在我们的配置中使用它。打开指向hardhat-tutorial
目录的终端并执行此命令
|
|
- 现在在文件夹中创建一个
.env
文件hardhat-tutorial
并添加以下行,使用注释中的说明获取您的 Alchemy API 密钥 URL 和 Rinkeby 私钥。确保您获取 Rinkeby 私钥的账户由 Rinkeby 以太币提供资金。
|
|
- 让我们还创建一个常量文件夹来跟踪我们拥有的任何常量。在
hardhat-tutorial
文件夹下创建一个名为的新文件夹constants
,并在该constants
文件夹下创建一个新文件index.js
- 在
index.js
文件中添加以下代码行。记得替换成你在教程中部署的代币合约ADDRESS-OF-CRYPTO-DEV-TOKEN
的合约地址Crypto Dev``ICO
|
|
- 让我们将合约部署到
rinkeby
网络。创建一个新文件,或替换现有的默认文件,deploy.js
在scripts
文件夹下命名 deploy.js
现在我们将编写一些代码来在文件中部署合约。
|
|
- 现在打开
hardhat.config.js
文件,我们将在rinkeby
此处添加网络,以便我们可以将合约部署到 rinkeby。hardhat.config.js
用下面给出的行替换文件中的所有行
|
|
- 编译合约,打开一个指向
hardhat-tutorial
目录的终端并执行这个命令
|
|
- 要部署,请打开指向
hardhat-tutorial
目录的终端并执行此命令
|
|
- 将打印在您的终端上的交易所合约地址保存在记事本中,您将在教程的后面进一步需要它。
网站
- 为了开发网站,我们将使用React和Next Js。React 是一个用于制作网站的 javascript 框架,Next Js 构建在 React 之上。
- 首先,您需要创建一个新
next
应用程序。您的文件夹结构应该类似于
|
|
- 要创建这个
my-app
,在终端指向 DeFi-Exchange 文件夹并输入
|
|
并按下enter
所有问题
- 现在运行应用程序,在终端中执行这些命令
|
|
- 现在转到
http://localhost:3000
,您的应用程序应该正在运行🤘 - 现在让我们安装 Web3Modal 库(https://github.com/Web3Modal/web3modal)。Web3Modal 是一个易于使用的库,可帮助开发人员通过简单的可自定义配置在其应用程序中添加对多个提供程序的支持。默认情况下,Web3Modal 库支持注入的提供程序,例如(Metamask、Dapper、Gnosis Safe、Frame、Web3 浏览器等),您还可以轻松配置库以支持 Portis、Fortmatic、Squarelink、Torus、Authereum、D’CENT 钱包和 Arkane。打开指向
my-app
目录的终端并执行此命令
|
|
- 在同一个终端也安装
ethers.js
|
|
- 在您的公用文件夹中,下载此图像并将其重命名为
cryptodev.svg
. - 现在转到样式文件夹并用
Home.modules.css
以下代码替换文件的所有内容,这将为您的 dapp 添加一些样式:
|
|
- 现在让我们创建一个常量文件夹来跟踪我们可能拥有的任何常量。
constants
在文件夹下创建一个文件my-app
夹,并在文件夹内constants
创建一个文件名 index.js - 粘贴以下代码。
- 替换为您在 ICO 教程中部署
ABI-CRYPTO-DEV-TOKEN-CONTRACT
的代币合约的 abi 。Crypto Dev
- 替换为您在 ICO 教程中部署
ADDRESS-OF-CRYPTO-DEV-TOKEN-CONTRACT
的代币合约的地址Crypto Dev
- 替换
ABI-EXCHANGE-CONTRACT
为交易所合约的 abi。要获取您的合同的 abi,请转到您的hardhat-tutorial/artifacts/contracts/Exchange.sol
文件夹并从您的Exchange.json
文件中获取标记在"abi"
密钥下的数组。 - 替换
ADDRESS-EXCHANGE-CONTRACT
为您在上面部署并保存到记事本的交换合约的地址
- 替换为您在 ICO 教程中部署
|
|
- 现在我们将创建一些实用程序文件来帮助我们更好地与合约交互。
utils
在文件夹内创建一个文件my-app
夹,在文件夹内创建 4 个文件:addLiquidity.js
、removeLiquidity.js
、getAmounts.js
和swap.js
- 让我们从编写一些代码开始
getAmounts.js
。此文件用于检索资产的余额和准备金
|
|
- 现在让我们为
addLiquidity.js
.addLiquidity
用于调用addLiquidity
合约中的函数增加流动性- 它还获得
Crypto Dev
用户批准用于合同的代币。代币需要批准的原因Crypto Dev
是因为它们是 ERC20 代币。合约从用户账户中提取 ERC20 需要用户账户的批准 calculateCD
告诉您对于给定数量的,可以添加Eth
多少代币到Crypto Dev``liquidity
- 我们通过保持一个比率来计算这一点。我们遵循的比例是
(amount of Crypto Dev tokens to be added) / (Crypto Dev tokens balance) = (Eth that would be added) / (Eth reserve in the contract)
- 所以通过数学我们得到
(amount of Crypto Dev tokens to be added) = (Eth that would be added * Crypto Dev tokens balance) / (Eth reserve in the contract)
- 需要该比率,以便增加流动性不会在很大程度上影响价格
- 注意
tx.wait()
表示我们正在等待交易被挖掘
|
|
- 现在添加一些代码
removeLiquidity.js
- 我们这里有两个函数:一个是
removeLiquidity
,另一个是getTokensAfterRemove
removeLiquidity
从合约中调用该removeLiquidity
函数,以移除LP
用户指定的代币数量getTokensAfterRemove
计算 从池中删除一定数量的代币Ether
后将CD
发送回用户的数量和代币LP
Eth
在用户提取代币后将返还给用户的金额LP
是根据一个比率计算的,- 比率是 ->
(amount of Eth that would be sent back to the user / Eth reserve) = (LP tokens withdrawn) / (total supply of LP tokens)
- 通过一些数学我们得到 ->
(amount of Eth that would be sent back to the user) = (Eth Reserve * LP tokens withdrawn) / (total supply of LP tokens)
- 同样,我们也为
CD
令牌保持一个比率,所以在我们的例子中 - 比率是 ->
(amount of CD tokens sent back to the user / CD Token reserve) = (LP tokens withdrawn) / (total supply of LP tokens)
- 然后
(amount of CD tokens sent back to the user) = (CD token reserve * LP tokens withdrawn) / (total supply of LP tokens)
- 我们这里有两个函数:一个是
|
|
-
现在是时候为
swap.js
我们的最后一个utils
文件编写代码了- 它有两个功能
getAmountOfTokenReceivedFromSwap
和swapTokens
swapTokens``Eth/Crypto Dev
用代币交换一定数量的代Crypto Dev/Eth
币- 如果
Eth
已经被用户从 UI 中选择,则表示用户有Eth
并且他想将其换成一定数量的Crypto Dev
代币 - 在这种情况下,我们调用该
ethToCryptoDevToken
函数。请注意,这Eth
是作为函数中的值发送的,因为用户将其支付Eth
给合约。Eth
在这种情况下,发送不是输入参数值 - 另一方面,如果
Eth
未选择,则表示用户希望将Crypto Dev
代币换成Eth
- 这里我们称
cryptoDevTokenToEth
getAmountOfTokensReceivedFromSwap
是一个函数,它计算给定一定数量的Eth/Crypto Dev
令牌,有多少Eth/Crypto Dev
令牌将被发送回用户- 如果
Eth
被选中,它会调用getAmountOfTokens
从合约中获取input
储备金和output
储备金。在这里,输入储备将是Eth
合约的余额,输出储备将是代Crypto Dev
币储备。Eth
如果未选中,则相反
- 它有两个功能
|
|
- 现在是我们应用程序的最后阶段的时候了,让我们将一些代码添加到
pages/index.js
接下来已经提供给您的文件中。将文件的所有内容替换为以下内容
|
|
- 现在在指向
my-app
文件夹的终端中,执行
|
|
您的 Exchange dapp 现在应该可以正常运行了🚀
将您的代码推送到 Github
确保在进行下一步部署之前将所有代码推送到 github
部署你的 dApp
我们现在将部署您的 dApp,以便每个人都可以看到您的网站,并且您可以与所有 LearnWeb3 DAO 朋友分享它。
-
转到 https://vercel.com/ 并使用您的 GitHub 登录
-
然后单击
New Project
按钮,然后选择您的 Defi-Exchange dApp 存储库 -
在配置您的新项目时,Vercel 将允许您自定义您的
Root Directory
-
单击
Edit
旁边Root Directory
并将其设置为my-app
-
点击
Deploy
-
现在,您可以通过转到仪表板、选择您的项目并从那里复制 URL 来查看您部署的网站!
如果你觉得这篇文章对你有所帮助,欢迎赞赏~
赞赏