前言
本文借鉴uniswap v2实现一个简化版的去中心化交易所的智能合约,交易所的核心恒定乘积自动做市商(CPAMM)
去中心化交易概念以及特征
去中心化交易 :在没有中心化中介机构的情况下进行的交易活动;
特征:
- 去中心化控制:所有交易都是通过区块链上的智能合约自动执行,避免了中央机构的操控和干预;
- 无需托管资产:交易通过用户的钱包直接完成,这大大降低了资金被盗或交易所跑路的风险
- 透明性和安全性:依赖于区块链技术,所有交易记录都是公开且不可篡改的,确保了交易的透明性和安全性
- 隐私性:不要求用户提供KYC(身份验证)信息,允许用户在不透露个人身份的情况下进行交易
- 无许可性:用户无需经过复杂的注册和身份验证过程即可参与交易
核心实现逻辑
- 自动化做市商(AMM)模型 :恒定乘积公式 x*y=k 来确定交易对的价格,确保了流动性池中的代币数量和价格之间的关系,使得交易价格自动调整以维持恒定乘积
- 流动性池 :每个交易对都有一个流动性池,由流动性提供者(LP)提供的两种代币组成,LP 通过将等值的两种代币存入池中来提供流动性
- 交易和费用 :用户在交易所上进行代币交换时,实际上是在与流动性池进行交易。每次交易都会支付一定的交易费用,这笔费用会按比例分配给所有流动性提供者,以奖励他们提供的流动性
合约开发
说明:实现流动性提供者和交易者
ini
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "hardhat/console.sol";
contract DEX is ERC20 {
// 代币合约
IERC20 public token0;
IERC20 public token1;
// 代币储备量
uint public reserve0;
uint public reserve1;
// 事件
event Mint(address indexed sender, uint amount0, uint amount1);
event Burn(address indexed sender, uint amount0, uint amount1);
event Swap(
address indexed sender,
uint amountIn,
address tokenIn,
uint amountOut,
address tokenOut
);
// 构造器,初始化代币地址
constructor(IERC20 _token0, IERC20 _token1) ERC20("DEX", "DEXTk") {
token0 = _token0;
token1 = _token1;
}
// 取两个数的最小值
function min(uint x, uint y) internal pure returns (uint z) {
z = x < y ? x : y;
}
// 计算平方根 babylonian method (https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method)
function sqrt(uint y) internal pure returns (uint z) {
if (y > 3) {
z = y;
uint x = y / 2 + 1;
while (x < z) {
z = x;
x = (y / x + x) / 2;
}
} else if (y != 0) {
z = 1;
}
}
// 添加流动性,转进代币,铸造LP
// 如果首次添加,铸造的LP数量 = sqrt(amount0 * amount1)
// 如果非首次,铸造的LP数量 = min(amount0/reserve0, amount1/reserve1)* totalSupply_LP
// @param amount0Desired 添加的token0数量
// @param amount1Desired 添加的token1数量
function addLiquidity(uint amount0Desired, uint amount1Desired) public returns(uint liquidity){
// 将添加的流动性转入Swap合约,需事先给Swap合约授权
token0.transferFrom(msg.sender, address(this), amount0Desired);
token1.transferFrom(msg.sender, address(this), amount1Desired);
// 计算添加的流动性
uint _totalSupply = totalSupply();
console.log(totalSupply());
if (_totalSupply == 0) {
// 如果是第一次添加流动性,铸造 L = sqrt(x * y) 单位的LP(流动性提供者)代币
liquidity = sqrt(amount0Desired * amount1Desired);
} else {
// 如果不是第一次添加流动性,按添加代币的数量比例铸造LP,取两个代币更小的那个比例
liquidity = min(amount0Desired * _totalSupply / reserve0, amount1Desired * _totalSupply /reserve1);
}
// 检查铸造的LP数量
require(liquidity > 0, 'INSUFFICIENT_LIQUIDITY_MINTED');
// 更新储备量
reserve0 = token0.balanceOf(address(this));
reserve1 = token1.balanceOf(address(this));
// 给流动性提供者铸造LP代币,代表他们提供的流动性
_mint(msg.sender, liquidity);
emit Mint(msg.sender, amount0Desired, amount1Desired);
}
// 移除流动性,销毁LP,转出代币
// 转出数量 = (liquidity / totalSupply_LP) * reserve
// @param liquidity 移除的流动性数量
function removeLiquidity(uint liquidity) external returns (uint amount0, uint amount1) {
// 获取余额
uint balance0 = token0.balanceOf(address(this));
uint balance1 = token1.balanceOf(address(this));
// 按LP的比例计算要转出的代币数量
uint _totalSupply = totalSupply();
amount0 = liquidity * balance0 / _totalSupply;
amount1 = liquidity * balance1 / _totalSupply;
// 检查代币数量
require(amount0 > 0 && amount1 > 0, 'INSUFFICIENT_LIQUIDITY_BURNED');
// 销毁LP
_burn(msg.sender, liquidity);
// 转出代币
token0.transfer(msg.sender, amount0);
token1.transfer(msg.sender, amount1);
// 更新储备量
reserve0 = token0.balanceOf(address(this));
reserve1 = token1.balanceOf(address(this));
emit Burn(msg.sender, amount0, amount1);
}
// 给定一个资产的数量和代币对的储备,计算交换另一个代币的数量
// 由于乘积恒定
// 交换前: k = x * y
// 交换后: k = (x + delta_x) * (y + delta_y)
// 可得 delta_y = - delta_x * y / (x + delta_x)
// 正/负号代表转入/转出
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) public pure returns (uint amountOut) {
require(amountIn > 0, 'INSUFFICIENT_AMOUNT');
require(reserveIn > 0 && reserveOut > 0, 'INSUFFICIENT_LIQUIDITY');
amountOut = amountIn * reserveOut / (reserveIn + amountIn);
}
// swap代币
// @param amountIn 用于交换的代币数量
// @param tokenIn 用于交换的代币合约地址
// @param amountOutMin 交换出另一种代币的最低数量
function swap(uint amountIn, IERC20 tokenIn, uint amountOutMin) external returns (uint amountOut, IERC20 tokenOut){
require(amountIn > 0, 'INSUFFICIENT_OUTPUT_AMOUNT');
require(tokenIn == token0 || tokenIn == token1, 'INVALID_TOKEN');
uint balance0 = token0.balanceOf(address(this));
uint balance1 = token1.balanceOf(address(this));
if(tokenIn == token0){
// 如果是token0交换token1
tokenOut = token1;
// 计算能交换出的token1数量
amountOut = getAmountOut(amountIn, balance0, balance1);
require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT');
// 进行交换
tokenIn.transferFrom(msg.sender, address(this), amountIn);
tokenOut.transfer(msg.sender, amountOut);
}else{
// 如果是token1交换token0
tokenOut = token0;
// 计算能交换出的token1数量
amountOut = getAmountOut(amountIn, balance1, balance0);
require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT');
// 进行交换
tokenIn.transferFrom(msg.sender, address(this), amountIn);
tokenOut.transfer(msg.sender, amountOut);
}
// 更新储备量
reserve0 = token0.balanceOf(address(this));
reserve1 = token1.balanceOf(address(this));
emit Swap(msg.sender, amountIn, address(tokenIn), amountOut, address(tokenOut));
}
}
合约测试
ini
const {ethers,getNamedAccounts,deployments} = require("hardhat");
const { assert,expect } = require("chai");
describe("DEX",function(){
let DEX;//去中心化交易所合约
let token;//代币1合约
let token1;//代币2合约
let addr1;//第一个账户
let addr2;//第一个账户
let firstAccount//第一个账户
let secondAccount//第二个账户
beforeEach(async function(){
await deployments.fixture(["token","token1","dex"]);//分别部署 token token1和dex的合约
[addr1,addr2]=await ethers.getSigners();//获取账号数组
firstAccount=(await getNamedAccounts()).firstAccount;//第一个账户
secondAccount=(await getNamedAccounts()).secondAccount;//第二个账户
const tokenDeployment = await deployments.get("MyToken");//代币1合约实例
token = await ethers.getContractAt("MyToken",tokenDeployment.address);//已经部署的合约交互
const token1Deployment = await deployments.get("MyToken1");//代币2合约实例
token1 = await ethers.getContractAt("MyToken1",token1Deployment.address);//已经部署的合约交互
const dexDeployment = await deployments.get("DEX");//去中心化交易所合约
DEX = await ethers.getContractAt("DEX",dexDeployment.address);//已经部署的合约交互
})
describe("DEX",function(){
it("DEX 交易", async function () {
// 查看代币1和代币2的余额
const balance1 = await token.balanceOf(addr1.address);//代币1的余额
const balance2 = await token1.balanceOf(addr1.address);//代币2的余额
console.log(ethers.utils.formatEther(balance1))
console.log(ethers.utils.formatEther(balance2))
// 代币1和代币2分别向去中心化交易所授权100个代币
const approveTx1 = await token.connect(addr1).approve(DEX.address, 100);
const approveTx2 = await token1.connect(addr1).approve(DEX.address, 100);
await approveTx1.wait();
await approveTx2.wait();
// 查看是否授权成功
const allowance1 = await token.allowance(addr1.address, DEX.address);
const allowance2 = await token1.allowance(addr1.address, DEX.address);
assert.equal(allowance1.toNumber(), 100, "Approval for token was not successful");
assert.equal(allowance2.toNumber(), 100, "Approval for token1 was not successful");
// 添加流动性
const addLiquidityTx = await DEX.addLiquidity(100, 100);
await addLiquidityTx.wait();
// 查看去中心化交易所合约的总供应量
// const totalSupply = await DEX.totalSupply();
// console.log("Total supply:", totalSupply.toString());
console.log('用户的LP份额',await DEX.balanceOf(addr1.address))
//交易所
//把token授权给交易所100个
const approveTx001 = await token.connect(addr1).approve(DEX.address, 100);
await approveTx001.wait();
// const approveTx002 = await token1.connect(addr1).approve(DEX.address, 100);
// await approveTx002.wait();
const swapTx =await DEX.swap(100,token.address,0);
await swapTx.wait();
// const swapTx1 =await DEX.swap(20,token1.address,0);
// await swapTx1.wait();
console.log("1存储量",await DEX.reserve0())
console.log("2存储量",await DEX.reserve1())
//移除流动性
console.log('用户的LP份额之前',await DEX.balanceOf(addr1.address))
const removeLiquidityTx = await DEX.removeLiquidity(80);
await removeLiquidityTx.wait();
console.log('销毁后用户的LP份额',await DEX.balanceOf(addr1.address))
});
})
})
# 测试指令
# npx hardhat test ./test/xxx.js
合约部署
ini
module.exports=async ({getNamedAccounts,deployments})=>{
const firstAccount= (await getNamedAccounts()).firstAccount;
const MyToken=await deployments.get("MyToken");
const TokenAddress = MyToken.address;
const MyToken1=await deployments.get("MyToken1");
const TokenAddress1 = MyToken1.address;
const {deploy,log} = deployments;
const DEX=await deploy("SimpleSwap",{
from:firstAccount,
args: [TokenAddress,TokenAddress1],//参数 代币1,代币2
log: true,
})
console.log("DEX合约",DEX.address)
}
module.exports.tags=["all","dex"]
# 部署指令
# npx hardhat deploy
总结
以上就是类(uinswap v2)去中心化交易所的开发、测试、部署的全过程。