# 实战案例四：DeFi 去中心化交易所

By [icepy](https://paragraph.com/@icepy) · 2022-03-20

---

现实情况是期望代币可以在去中心化的交易场所中交换，这篇文章就是从一个简单案例来说明交换，流动性该如何实现。

我们需要先梳理一下，期望这个应用具备哪些功能：

*   只用一个代币对建立交易场所
    
*   交易收取 1% 的费用
    
*   用户可以为 UseWeb3Token 添加或删除流动性
    
*   为用户提供 LP 代币
    

> 说明：实现会比这个例子复杂的多

    // SPDX-License-Identifier: SEE LICENSE IN LICENSE
    pragma solidity ^0.8.4;
    
    import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
    
    contract UseWeb3Exchange is ERC20 {
      address public useweb3TokenAddress;
    
      constructor(address useweb3TokenContract) ERC20("LP Token", "LP") {
        useweb3TokenAddress = useweb3TokenContract;  
      }
    
      function getReserve() public view returns(uint256){
        return ERC20(useweb3TokenAddress).balanceOf(address(this));
      }
    }
    

因为要提供 LP 所以直接继承 ERC20

同时定义 `getReserve` 获取当前合约持有的 UseWeb3Token 数量

    function getReserve() public view returns(uint256){
      return ERC20(useweb3TokenAddress).balanceOf(address(this));
    }
    

接下来，我们开始编写添加流动性方法

    function addLiquidity(uint256 _amount) public payable returns(uint256){
      uint256 liquidity;
      // 用当前合约的 eth 做初始值
      uint256 ethBalance = address(this).balance;
      // 查询当前合约持有 useweb3Token 的数量
      uint256 currentContractHoldUseweb3TokenReserve = getReserve();
      ERC20 useweb3Token = ERC20(useweb3TokenAddress);
      // currentContractHoldUseweb3TokenReserve = 0 表示第一次添加流动性
      if (currentContractHoldUseweb3TokenReserve == 0){
        // 将 _amount 数量 从发起者地址转移到当前合约地址 
        useweb3Token.transferFrom(msg.sender, address(this), _amount);
        liquidity = ethBalance;
        // 给发起者地址 LP 代币
        _mint(msg.sender, liquidity);
      } else {
        // 已经存在流动性时的处理逻辑
    
        // 当前合约的 eth 数量 - 用户发送来的 eth 数量
        uint256 ethReserve = ethBalance - msg.value;
        // （用用户发送来的 eth 数量乘以当前合约持有的 useweb3Token的数量）除以（已经减过的eth储备） ，获取一定比率的 useweb3Token
        uint256 useweb3TokenAmount = (msg.value * currentContractHoldUseweb3TokenReserve)/(ethReserve);
        // 检查 _amount 参数必须大于 计算出来的 useweb3Token 的数量
        require(_amount > useweb3TokenAmount, "Amount of tokens sent is less than the minimum tokens required");
        useweb3Token.transferFrom(msg.sender, address(this), useweb3TokenAmount);
        // (LP 代币的总量 * 用户发送来的 eth 数量)除以（已经减过的eth储备）
        liquidity = (totalSupply() * msg.value)/ethReserve;
        // 给发送者地址 LP 代币
        _mint(msg.sender, liquidity);
      }
      return liquidity;
    }
    

删除流动性方法

    function removeLiquidity(uint256 _amount) public returns(uint256, uint256){
      require(_amount > 0, "amount > 0");
      // 查询当前合约持有的 eth 数量
      uint256 ethReserve = address(this).balance;
      uint256 lpTotal = totalSupply();
    
      uint256 ethAmount = (ethReserve * _amount)/lpTotal;
      uint256 useweb3TokenAmount = (getReserve() * _amount)/lpTotal;
    
      // 烧毁用户的 lp 代币
      _burn(msg.sender, _amount);
    
      // 将 eth 和 useweb3Token 代币还给用户
      payable(msg.sender).transfer(ethAmount);
      ERC20(useweb3TokenAddress).transfer(msg.sender, useweb3TokenAmount);
      return (ethAmount, useweb3TokenAmount);
    }
    

交易收取费用的方法

    function getAmountOfTokens(uint256 inputAmount, uint256 inputReserve, uint256 outputReserve) public pure returns(uint256){
      require(inputReserve > 0 && outputReserve > 0, "invalid reserves");
      uint256 inputAmountWithFee = inputAmount * 99;
      uint256 numerator = inputAmountWithFee * outputReserve;
      uint256 denominator = (inputReserve * 100) + inputAmountWithFee;
      return numerator / denominator;
    }
    

交换的方法

    function ethToUseWeb3Token(uint256 minTokens) public payable{
      uint256 currentContractHoldUseweb3TokenReserve = getReserve();
      uint256 tokensBought = getAmountOfTokens(msg.value, address(this).balance - msg.value, currentContractHoldUseweb3TokenReserve);
      require(tokensBought >= minTokens, "insufficient output amount");
      ERC20(useweb3TokenAddress).transfer(msg.sender, tokensBought);
    }
    
    function useWeb3TokenToEth(uint256 tokenSold, uint256 minEth) public{
      uint256 currentContractHoldUseweb3TokenReserve = getReserve();
      uint256 ethBought = getAmountOfTokens(tokenSold, currentContractHoldUseweb3TokenReserve, address(this).balance);
      require(ethBought >= minEth, "insufficient output amount");
      ERC20(useweb3TokenAddress).transferFrom(msg.sender, address(this), tokenSold);
      payable(msg.sender).transfer(ethBought);
    }
    

观察阶段
----

我们编写一个简单的前端代码来看一看合约运行的结果，需要注意的是，我们需要为交易合约授权。

![](https://storage.googleapis.com/papyrus_images/c43a2e8f008d32c9003dcde3ee183f2fa5de8bd5f5b55bffe5d9f075b9e0d52c.png)

    const web3Provider = getWeb3Provider();
    const signer = web3Provider.getSigner();
    const exchangeContract = new ethers.Contract(USEWEB3_EXCHANGE_ADDRESS, USEWEB3_EXCHANGE_ABI, signer); 
    const tokenContract = new ethers.Contract(USEWEB3_TOKEN_ADDRESS, USEWEB3_TOKEN_ABI, signer);
    
    const tokenTx = await tokenContract.approve(USEWEB3_EXCHANGE_ADDRESS, ethers.utils.parseEther("10"));
    await tokenTx.wait();
    

然后初始添加流动性

    const exchangeTx = await exchangeContract.addLiquidity(ethers.utils.parseEther("10"), {value: ethers.utils.parseEther("0.01")});
    await exchangeTx.wait();
    

![](https://storage.googleapis.com/papyrus_images/b777519e2c03b0b094af86fc21204b8548f5fd7f62c3fe98cced3c4a4b04008e.png)

通过 [https://rinkeby.etherscan.io/](https://rinkeby.etherscan.io/) 来观察最后的结果

![](https://storage.googleapis.com/papyrus_images/612509ab63c8781e8dd2d23db91a14cab9bfff8d3cd65bb5ebe83d6f1b499110.png)

![](https://storage.googleapis.com/papyrus_images/e81ed4537a5f366cc78fec5716241fea2e647651415c19967bbaf8fa80bc95b6.png)

---

*Originally published on [icepy](https://paragraph.com/@icepy/defi)*
