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