Uniswap是什么
简单来说,Uniswap 是一个去中心化交易所 (DEX),旨在成为一个中心化交易所的替代方案。 它运行在以太坊区块链上,并且是完全自动化的:没有管理员、经理或具有特权访问权限的用户。
从底层来看,Uniswap是一系列算法,这些算法让用户能够创建资金池(交易对)、向资金池中注入流动性,从而使所有用户能够通过这些资金池来交易代币。这些算法被称为“自动做市商”或“自动流动性提供者”。
下面谈一谈“做市商”。
做市商是向市场提供流动性(可供交易的资产)的实体。正是流动性让交易得以够实现,如果你想卖某种东西,但没人愿意买这种东西,那么交易就不会成立,反之亦然。
一个去DEX(中心化交易所)必须提供大量的流动性才能够提供像中心化交易所一样的服务。获得流动性的一种方法是DEX的开发人员将自己的钱(或投资者的钱)投入其中并成为做市商。然而,这不是一个现实的解决方案,因为考虑到DEX允许任何代币之间的交换,他们需要大量资金为所有货币对提供足够的流动性。此外,这将使DEX中心化:作为唯一的做市商,开发团队手中将拥有很大的权力。
更好的解决方案是允许任何人成为做市商。这就是使 Uniswap 成为自动做市商的原因:任何用户都可以将他们的资金存入交易对并从中受益。
Uniswap 扮演的另一个重要角色是价格预言机。价格预言机是从中心化交易所获取代币价格并将其提供给智能合约。由于Uniswap具备喂价功能,它可以充当二级市场,吸引套利者从Uniswap和中心化交易所的价格差异中套利。
中心化交易所的价格通常很难操纵,因为中心化交易所的交易量通常非常大。然而即便Uniswap没有那么大的交易量,却仍然可以提供价格预言机的功能。
恒定乘积做市商模型
自动做市商是一个广泛的概念,包含不同的去中心化做市商算法。Uniswap采用的是恒定乘积做市商模型。
Uniswap的核心原理是一条恒定乘积的公式,其中k是一个常数:
无论一个交易对中的token余额和ETH余额怎样变化,由两个余额得到的k始终不变。
这个公式同样也实现了计算兑换价格的功能。
创建项目
下面我们将使用Hardhat框架和Solidity 0.8.4编写智能合约来逐步实现Uniswap的各个功能。
新建SwapV1Clone文件夹,执行以下命令:
yarn add -D hardhat
yarn add -D @openzeppelin/contracts
yarn hardhat
删除contract,script和test文件夹中的所有文件。
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本身只包含Factory和Exchange两个合约。
Factory合约有以下功能:
- 创建ETH-token交易对。
- 根据token地址查找所属交易对地址。
- 根据交易对地址查询其中的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)