快速实现一个去中心交易所的智能合约

前言

本文借鉴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)去中心化交易所的开发、测试、部署的全过程。

相关推荐
Code blocks9 小时前
小试牛刀-SpringBoot集成SOL链
java·区块链·springboot
YSGZJJ9 小时前
2025年股指期货每月什么时候交割?
区块链
羡君19 小时前
一文理解区块链
区块链
ClonBrowser1 天前
Facebook元宇宙项目中的智能合约应用:提升虚拟空间的自治能力
智能合约·元宇宙·facebook·隐私保护
koko爱英语1 天前
区块链安全常见的攻击分析——拒绝服务攻击 (Denial of Service-DOS)King合约【11】
安全·区块链
默辨1 天前
浅谈分布式共识算法
分布式·区块链·共识算法
加密新世界1 天前
深入 Solana 共识 - 从分叉到最终确定性
区块链
Roun32 天前
拆解 Web3:探寻去中心化网络的核心密码
web3·去中心化·区块链
GISer_Jing2 天前
实景三维&点云处理专业软件&ArcGIS根据DSM生成地表点云集
python·arcgis·web3