Defi编程——Uniswap V1(1)

877 阅读6分钟

原文:jeiwan.net/posts/progr…

Uniswap是什么

简单来说,Uniswap 是一个去中心化交易所 (DEX),旨在成为一个中心化交易所的替代方案。 它运行在以太坊区块链上,并且是完全自动化的:没有管理员、经理或具有特权访问权限的用户。

从底层来看,Uniswap是一系列算法,这些算法让用户能够创建资金池(交易对)、向资金池中注入流动性,从而使所有用户能够通过这些资金池来交易代币。这些算法被称为“自动做市商”或“自动流动性提供者”。

下面谈一谈“做市商”。

做市商是向市场提供流动性(可供交易的资产)的实体。正是流动性让交易得以够实现,如果你想卖某种东西,但没人愿意买这种东西,那么交易就不会成立,反之亦然。

一个去DEX(中心化交易所)必须提供大量的流动性才能够提供像中心化交易所一样的服务。获得流动性的一种方法是DEX的开发人员将自己的钱(或投资者的钱)投入其中并成为做市商。然而,这不是一个现实的解决方案,因为考虑到DEX允许任何代币之间的交换,他们需要大量资金为所有货币对提供足够的流动性。此外,这将使DEX中心化:作为唯一的做市商,开发团队手中将拥有很大的权力。

更好的解决方案是允许任何人成为做市商。这就是使 Uniswap 成为自动做市商的原因:任何用户都可以将他们的资金存入交易对并从中受益。

Uniswap 扮演的另一个重要角色是价格预言机。价格预言机是从中心化交易所获取代币价格并将其提供给智能合约。由于Uniswap具备喂价功能,它可以充当二级市场,吸引套利者从Uniswap和中心化交易所的价格差异中套利。

中心化交易所的价格通常很难操纵,因为中心化交易所的交易量通常非常大。然而即便Uniswap没有那么大的交易量,却仍然可以提供价格预言机的功能。

恒定乘积做市商模型

自动做市商是一个广泛的概念,包含不同的去中心化做市商算法。Uniswap采用的是恒定乘积做市商模型。

Uniswap的核心原理是一条恒定乘积的公式,其中k是一个常数:

token余额ETH余额=ktoken余额 ∗ ETH余额 = k

无论一个交易对中的token余额和ETH余额怎样变化,由两个余额得到的k始终不变。

这个公式同样也实现了计算兑换价格的功能。

创建项目

下面我们将使用Hardhat框架和Solidity 0.8.4编写智能合约来逐步实现Uniswap的各个功能。

新建SwapV1Clone文件夹,执行以下命令:

yarn add -D hardhat
yarn add -D @openzeppelin/contracts
yarn hardhat

删除contract,scripttest文件夹中的所有文件。

Token合约

下面我们基于OpenZeppelin的ERC20合约,部署一个ERC20代币作为交易对的token,在部署token的同时向调用者发送指定指定数量的token。

contracts文件夹中新建Token.sol文件,加入以下代码。

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract Token is ERC20 {
    constructor(
        string memory name,
        string memory symbol,
        uint256 initialSupply
    ) ERC20(name, symbol) {
        _mint(msg.sender, initialSupply);
    }
}

Exchange合约

Uniswap V1本身只包含FactoryExchange两个合约。

Factory合约有以下功能:

  1. 创建ETH-token交易对。
  2. 根据token地址查找所属交易对地址。
  3. 根据交易对地址查询其中的token的地址。

Exchange合约实现交易功能。

每一个ETH-token交易对都只支持ETH和一种代币的交易。因此我们可以将交易对中的token的地址作为交易对的索引。

下面我们实现Exchange合约的部分功能。

contracts文件夹中新建Exchange.sol文件,加入以下代码:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

contract Exchange {
    address public tokenAddress;

    constructor(address _token) {
        require(_token != address(0), "invalid token address");

        tokenAddress = _token;
    }
}

在以上代码中,我们在创建交易对时接收一个token地址,并将其保存在一个public关键字的变量中,这样就可以在外部访问该变量来查询该交易对交易的币种。

添加流动性

前面说过,要想支持双向的交易,需要有流动性。

下面在Exchange合约中加入添加流动性,即向Exchange合约发送ETH和token的方法addLiquidity

为了向Exchange合约发送ETH,我们给addLiquidity方法加上payable关键字,使得调用addLiquidity方法时可以顺带向Exchange合约发送ETH。

由于token保存在Token合约,因此需要调用Token合约的transferFrom才能将测试账户名下的token划转到Exchange合约名下。

代码如下:

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract Exchange {
    // constructor()...

    function addLiquidity(uint256 _tokenAmount) public payable {
        IERC20 token = IERC20(tokenAddress);
        token.transferFrom(msg.sender, address(this), _tokenAmount);
    }
}

测试添加流动性

下面我们测试一下前面的逻辑是否正确。

test文件夹下新建Exchange.text.ts文件。添加如下代码:

import { expect } from "chai";
import { ethers } from "hardhat";

import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { BigNumber } from "@ethersproject/bignumber";

const toWei = (value: number | string | BigNumber) => {
	return ethers.utils.parseEther(value.toString());
};

const getEthBalance = ethers.provider.getBalance;

describe("Exchange", () => {
	const deploymentFixture = async () => {
		// 获取一个Hardhat提供的测试地址
		const [liquidityProvider] = await ethers.getSigners();

		// 创建一个token,并向测试账户转账
		const tokenFactory = await ethers.getContractFactory("Token");
		const token = await tokenFactory.deploy("Token", "TKN", toWei(1000000));
		await token.deployed();

		// 创建一个交易对
		const exchangeFactory = await ethers.getContractFactory("Exchange");
		const exchange = await exchangeFactory.deploy(token.address);
		await exchange.deployed();

		return {
			tokenFactory,
			token,
			exchangeFactory,
			exchange,
			liquidityProvider,
		};
	};

	describe("addLiquidity", async () => {
		it("adds liquidity", async () => {
      // 测试逻辑...
    });
	});
});

在测试中,我们将向交易对合约添加200个ETH和200个token,然后查看交易对合约中是否增加了上述数量的ETH和token。

Exchange合约执行划转操作之前,需要调用Token合约的approve方法给Exchange合约分配操纵token的额度。

测试逻辑的代码如下:

describe("addLiquidity", async () => {
  it("adds liquidity", async () => {
    // 给交易对合约分配操纵200个token的额度
    await token.approve(exchange.address, toWei(200));
  	// 调用addLiquidity方法添加流动性,添加200个token和100个eth
    await exchange.addLiquidity(toWei(200), { value: toWei(100) });

    // 查询交易对中的eth余额
    expect(await getBalance(exchange.address)).to.equal(toWei(100));
    // 查询交易对中的token余额
    expect(await exchange.getReserve()).to.equal(toWei(200));
  });
});

执行如下命令进行测试:

yarn hardhat test

如果顺利,可以看到控制台出现如下提示,说明测试通过。

  Exchange
    addLiquidity
      ✔ adds liquidity (368ms)


  1 passing (373ms)