构建你自己的去中心化交易所
现在是时候为您的 Crypto Dev 代币启动 DeFi 交易所了
要求
- 仅使用一个资产对(Eth / Crypto Dev)建立交易所
- 您的去中心化交易所应收取 1% 的掉期费用
- 当用户增加流动性时,应给予他们 Crypto Dev LP 代币(流动性提供者代币)
- CD LP 代币应与 Ether 用户愿 意增加流动性的比例成比例
让我们开始。
预备
- 您已经完成了之前的 ICO 教程
- 您已经完成了之前的介绍和深入了解去中心化交易所教程
- 您已完成之前的提供者、签名者、ABI 和批准流程教程
智能合约
创建项目
mkdir DeFi-Exchange
cd DeFi-Exchange
mkdir hardhat-tutorial
cd hardhat-tutorial
npm init --yes
npm install --save-dev hardhat
npx hardhat
npm install @openzeppelin/contracts
创建 Exchange.sol
导入继承 ERC20.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract Exchange is ERC20 {
}
创建构造函数
- 它将您在 ICO 教程中部署的 _CryptoDevToken 的地址作为输入参数
- 然后检查地址是否为空地址
- 在所有检查之后,它将值分配给 cryptoDevTokenAddress 变量的输入参数
构造函数还为我们的 Crypto Dev LP 代币设置名称和符号
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract Exchange is ERC20 {
address public cryptoDevTokenAddress;
// Exchange is inheriting ERC20, because our exchange would keep track of Crypto Dev LP tokens
constructor(address _CryptoDevtoken) ERC20("CryptoDev LP Token", "CDLP") {
require(_CryptoDevtoken != address(0), "Token address passed is a null address");
cryptoDevTokenAddress = _CryptoDevtoken;
}
}
创建代币储备方法
是时候创建一个函数来获取合约持有的 Eth 和 Crypto Dev 代币的储备了。
ETH 储备金将等于合约的余额,可以使用 address(this).balance 找到,因此我们只需创建一个函数,仅用于获取 Crypto Dev 代币的储备金
我们知道我们部署的 Crypto Dev Token 合约是 ERC20。 所以我们可以调用 balanceOf 来查看合约持有的 CryptoDev Token 的余额
/**
* @dev Returns the amount of `Crypto Dev Tokens` held by the contract
*/
function getReserve() public view returns (uint) {
return ERC20(cryptoDevTokenAddress).balanceOf(address(this));
}
创建添加流动性函数
它将以 ETH 和 Crypto Dev 代币的形式将流动性添加到我们的合约中
如果 cryptoDevTokenReserve
为零,意味着这是第一次有人将 Crypto Dev 代币和 ETH 添加到合约中。在这种情况,我们不必保持代币之间的比率,因为我们没有任何流动性。因此,我们接受用户在初始调用中发送的任何数量的令牌
如果 cryptoDevTokenReserve
不为零,那么我们必须确保当有人增加流动性时,它不会影响市场当前的价格。为了确保这一点,我们 维持一个必须保持不变的比率。比例为(cryptoDevTokenAmount user can add/cryptoDevTokenReserve in the contract) = (Eth Sent by the user/Eth Reserve in the contract)
。
这个比率决定了在给定一定数量的 ETH 的情况下,用户可以提供多少 Crypto Dev 代币。当用户增加流动性时,我们需要为他提供一些 LP 代币,因为我们需要跟踪他提供给合约的流动性数量。铸造给用户的 LP 代币数量与用户提供的 ETH 成正比。
在初始流动性情况下,当没有流动性时:将铸造给用户的 LP 代币数量等于合约的 ETH 余额(因为余额等于用户在 addLiquidity 调用中发送的 ETH)
当合约中已经存在流动性时,铸造的 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)
如果这让您感到有些困惑,请再次打开之前的理论课并复习 xy = k 价格函数和 LP 代币背后的数学。
/**
* @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 proportional 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;
}
创建移除流动性方法
将发送回用户的以太币数量将基于一个比率。 该比率是(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 代币数量将被烧毁
/**
* @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 the contract to the user's wallet
payable(msg.sender).transfer(ethAmount);
// Transfer `cryptoDevTokenAmount` of Crypto Dev tokens from the contract to the user's wallet
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
在我们的例子中是要接收的代币,Δx = ((input amount)*99)/100
,x = 输入储备,y = 输出储备- 输入储备和输出储备将取决于我们正在实施的交换。 ETH 到 Crypto Dev 令牌,反之亦然
/**
* @dev Returns the amount Eth/Crypto Dev tokens that would be returned to the user
* in the swap
*/
function getAmountOfTokens(
uint256 inputAmount,
uint256 inputReserve,
uint256 outputReserve
) public pure returns (uint256) {
require(inputReserve > 0 && outputReserve > 0, "invalid reserves");
// We are charging a fee of `1%`
// Input amount with fee = (input amount - (1*(input amount)/100)) = ((input amount)*99)/100
uint256 inputAmountWithFee = inputAmount * 99;
// Because we need to follow the concept of `XY = K` curve
// We need to make sure (x + Δx) * (y - Δy) = x * y
// So the final formula is Δy = (y * Δx) / (x + Δx)
// Δy in our case is `tokens to be received`
// Δx = ((input amount)*99)/100, x = inputReserve, y = outputReserve
// So by putting the values in the formulae you can get the numerator and denominator
uint256 numerator = inputAmountWithFee * outputReserve;
uint256 denominator = (inputReserve * 100) + inputAmountWithFee;
return numerator / denominator;
}
ETH 换 Crypto Dev 代币
/**
* @dev Swaps Eth for CryptoDev Tokens
*/
function ethToCryptoDevToken(uint _minTokens) public payable {
uint256 tokenReserve = getReserve();
// call the `getAmountOfTokens` to get the amount of Crypto Dev tokens
// that would be returned to the user after the swap
// Notice that the `inputReserve` we are sending is equal to
// `address(this).balance - msg.value` instead of just `address(this).balance`
// because `address(this).balance` already contains the `msg.value` user has sent in the given call
// so we need to subtract it to get the actual input reserve
uint256 tokensBought = getAmountOfTokens(
msg.value,
address(this).balance - msg.value,
tokenReserve
);
require(tokensBought >= _minTokens, "insufficient output amount");
// Transfer the `Crypto Dev` tokens to the user
ERC20(cryptoDevTokenAddress).transfer(msg.sender, tokensBought);
}
Crypto Dev 换 ETH 代币
/**
* @dev Swaps CryptoDev Tokens for Eth
*/
function cryptoDevTokenToEth(uint _tokensSold, uint _minEth) public {
uint256 tokenReserve = getReserve();
// call the `getAmountOfTokens` to get the amount of Eth
// that would be returned to the user after the swap
uint256 ethBought = getAmountOfTokens(
_tokensSold,
tokenReserve,
address(this).balance
);
require(ethBought >= _minEth, "insufficient output amount");
// Transfer `Crypto Dev` tokens from the user's address to the contract
ERC20(cryptoDevTokenAddress).transferFrom(
msg.sender,
address(this),
_tokensSold
);
// send the `ethBought` to the user from the contract
payable(msg.sender).transfer(ethBought);
}
最终合约代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract Exchange is ERC20 {
address public cryptoDevTokenAddress;
// Exchange is inheriting ERC20, because our exchange would keep track of Crypto Dev LP tokens
constructor(address _CryptoDevtoken) ERC20("CryptoDev LP Token", "CDLP") {
require(_CryptoDevtoken != address(0), "Token address passed is a null address");
cryptoDevTokenAddress = _CryptoDevtoken;
}
/**
* @dev Returns the amount of `Crypto Dev Tokens` held by the contract
*/
function getReserve() public view returns (uint) {
return ERC20(cryptoDevTokenAddress).balanceOf(address(this));
}
/**
* @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` address 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);
} 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
// Ration 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;
}
/**
* @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 the contract to the user's wallet
payable(msg.sender).transfer(ethAmount);
// Transfer `cryptoDevTokenAmount` of `Crypto Dev` tokens from the contract to the user's wallet
ERC20(cryptoDevTokenAddress).transfer(msg.sender, cryptoDevTokenAmount);
return (ethAmount, cryptoDevTokenAmount);
}
/**
* @dev Returns the amount Eth/Crypto Dev tokens that would be returned to the user
* in the swap
*/
function getAmountOfTokens(
uint256 inputAmount,
uint256 inputReserve,
uint256 outputReserve
) public pure returns (uint256) {
require(inputReserve > 0 && outputReserve > 0, "invalid reserves");
// We are charging a fee of `1%`
// Input amount with fee = (input amount - (1*(input amount)/100)) = ((input amount)*99)/100
uint256 inputAmountWithFee = inputAmount * 99;
// Because we need to follow the concept of `XY = K` curve
// We need to make sure (x + Δx) * (y - Δy) = x * y
// So the final formula is Δy = (y * Δx) / (x + Δx)
// Δy in our case is `tokens to be received`
// Δx = ((input amount)*99)/100, x = inputReserve, y = outputReserve
// So by putting the values in the formulae you can get the numerator and denominator
uint256 numerator = inputAmountWithFee * outputReserve;
uint256 denominator = (inputReserve * 100) + inputAmountWithFee;
return numerator / denominator;
}
/**
* @dev Swaps Eth for CryptoDev Tokens
*/
function ethToCryptoDevToken(uint _minTokens) public payable {
uint256 tokenReserve = getReserve();
// call the `getAmountOfTokens` to get the amount of Crypto Dev tokens
// that would be returned to the user after the swap
// Notice that the `inputReserve` we are sending is equal to
// `address(this).balance - msg.value` instead of just `address(this).balance`
// because `address(this).balance` already contains the `msg.value` user has sent in the given call
// so we need to subtract it to get the actual input reserve
uint256 tokensBought = getAmountOfTokens(
msg.value,
address(this).balance - msg.value,
tokenReserve
);
require(tokensBought >= _minTokens, "insufficient output amount");
// Transfer the `Crypto Dev` tokens to the user
ERC20(cryptoDevTokenAddress).transfer(msg.sender, tokensBought);
}
/**
* @dev Swaps CryptoDev Tokens for Eth
*/
function cryptoDevTokenToEth(uint _tokensSold, uint _minEth) public {
uint256 tokenReserve = getReserve();
// call the `getAmountOfTokens` to get the amount of Eth
// that would be returned to the user after the swap
uint256 ethBought = getAmountOfTokens(
_tokensSold,
tokenReserve,
address(this).balance
);
require(ethBought >= _minEth, "insufficient output amount");
// Transfer `Crypto Dev` tokens from the user's address to the contract
ERC20(cryptoDevTokenAddress).transferFrom(
msg.sender,
address(this),
_tokensSold
);
// send the `ethBought` to the user from the contract
payable(msg.sender).transfer(ethBought);
}
}
安装 dotenv 配置环境变量
npm i dotenv
.env
QUICKNODE_HTTP_URL="add-quicknode-http-provider-url-here"
PRIVATE_KEY="add-the-private-key-here"
配置参数
constants/index.js
const CRYPTO_DEV_TOKEN_CONTRACT_ADDRESS = "ADDRESS-OF-CRYPTO-DEV-TOKEN"; // ICO 示例中部署的地址
module.exports = { CRYPTO_DEV_TOKEN_CONTRACT_ADDRESS };
编写部署代码
scripts/deploy.js
const { ethers } = require("hardhat");
require("dotenv").config({ path: ".env" });
const { CRYPTO_DEV_TOKEN_CONTRACT_ADDRESS } = require("../constants");
async function main() {
const cryptoDevTokenAddress = CRYPTO_DEV_TOKEN_CONTRACT_ADDRESS;
/*
A ContractFactory in ethers.js is an abstraction used to deploy new smart contracts,
so exchangeContract here is a factory for instances of our Exchange contract.
*/
const exchangeContract = await ethers.getContractFactory("Exchange");
// here we deploy the contract
const deployedExchangeContract = await exchangeContract.deploy(
cryptoDevTokenAddress
);
await deployedExchangeContract.deployed();
// print the address of the deployed contract
console.log("Exchange Contract Address:", deployedExchangeContract.address);
}
// Call the main function and catch if there is any error
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});