Uniswap介绍

介绍

引用官方文档的定义

The Uniswap protocol is a peer-to-peer system designed for exchanging cryptocurrencies (ERC-20 Tokens) on the Ethereum blockchain. The protocol is implemented as a set of persistent, non-upgradable smart contracts; designed to prioritize censorship resistance, security, self-custody, and to function without any trusted intermediaries who may selectively restrict access.

Uniswap是一个点对点的去中心化加密货币(或者说是符合ERC20协议的货币)交易系统,用户能使用这个交易所自由地完成各种ERC20 token之间或ETH的兑换。

应用链接:https://app.uniswap.org/

发展历程

Uniswap想法诞生:2017年6月22日,Vitalik发表文章《On Path Independence》

Uniswap创世人登场:2017年7月6日,Hayden Adams被西门子解雇,在其朋友Karl Floersch的影响下开始关注以太坊,并学习智能合约开发。

Uniswap-v1面世:2018年,在Hayden Adams与以太坊有关的人士讨论不断完善并在自身努力下,Uniswap-v1版本于11月2日部署到以太坊主网。

Uniswap的发展:自2020年后,Uniswap逐渐推出新的版本从v2到如今处于开发中的v4版本。Uniswap已经成为以太坊上交易量最大的DEX之一。

人物介绍:Hayden Adams ,是 Uniswap 的创始人之一,也是该项目的重要推动者和领导者。Adams 在加利福尼亚州长大,拥有工程和计算机科学的背景。

Adams 在2018年创建了 Uniswap,并在其初期的开发过程中积极参与。他在设计 Uniswap 时采用了自动化市场制造商(AMM)的概念,使得任何人都可以提供流动性,并从交易费中获取收益,而无需依赖传统的市场制造商或中介。

随着 Uniswap 的发展壮大,Adams 的领导和技术指导成为了项目成功的重要因素之一。他致力于推动项目的不断创新和发展,并与团队合作,不断改进 Uniswap 协议,以满足用户和市场的需求。

Adams 的努力和才华使得 Uniswap 成为了全球范围内 DeFi 生态系统中最重要和受欢迎的项目之一,同时也为加密货币和区块链技术的发展做出了重要贡献。

(人物介绍由ChatGPT生成,详细的人物故事和Uniswap的诞生过程可参考《A Short History of Uniswap》

Uniswap-v1原理

自动做市商(automated market makers,AMM)

在上述Uniswap的发展历程中中介绍到其设计灵感来源于《On Path Independence》,该文章描述了链上做市商的方式——即,AMM。

AMM是组成DeFi生态系统的一部分,它以一个简单的数学公式(恒定乘积公式,Constant Product Formula)为核心实现无许可和自动化的数字资产交易。

流动性(Liquidity):在AMM中的流动性是指将一种资产转化为另一种资产,这里资产可以是以太坊上的erc20代币

流动性提供者(Liquidity Provider,LP):LP也可认为是做市商,LP一般会为添加一对代币池子以供交易。LP可以通过向交易者收交易手续费,作为无偿损失的补偿。

恒定乘积公式

恒定乘积公式:x*y = k

  • x和y代表流动性池中的token的总量
  • 不管交易怎样变化,市场始终无法脱离这条曲线
  • 交易失去token和获得token只与交易前后的状态有关,与交易价格变化路径无关

恒定乘积公式

通过恒定乘积公式我们能计算交易者swap买卖一种token获得另一种token的数量、LP为流动性池子添加流动性和移除流动性获得的Share

这里的share是指实现AMM的合约发行的token,这个token也可以理解为LP为池子的流动性量化体现。计算:share = √ ( x * y ) = √ k。

LP可以通过添加流动性mint share,通过移除流动性burn share

LP取回池中的一定数量的token对(使用移除流动性方法),需要通过恒定乘积公式去burn一定数量的share

添加流动性

LP向池中添加流动性,根据添加token对x与y的数量去计算需要mint的share数量。我们需要保证添加流动性时,池中价格保持不变,即p(添加前) = x/y 等于p(移除后) (x+dx)/(y+dy)

添加流动性1

添加流动性2

交易

不管交易者在池中买卖token多少token,始终满足x * y = k。我们可以通过该公式计算卖dx计算得到dy

swap

移除流动性

当LP想要拿回池中的token对时,需要移除池中的流动性。我们需要确保移除流动时保持价格不变,即p(移除前) = x/y 等于p(移除后) (x-dx)/(y-dy)

移除流动性

Uniswap-v1代码

Uniswap v1使用vyper语言编写,这里我将使用solidity重构Uniswap的代码对其进行分析

v1的代码有两部分:Exchange和Factory

Exchange:Token和ETH代币对,相当与一个池子,能进行添加、移除流动性和uniswap

Factory:用与创建新的Toekn与ETH的代币对,也可以查询代币对对应的Exchange

Exchange

exchange实现了ERC20标准,该合约通过添加ETH和Token流动性铸造代币,移除流动性销毁代币以获得ETH和Token。(合约铸造铸造代币下文成为LPT)此外合约提供了ETH与Token的代币兑换、Token与ETH的代币兑换、Token与ETH的价格和Token与另一种Token的兑换等方法

添加/移除流动性

addLiquidity(uint256 min_liquidity, uint256 max_tokens, uint256 deadline) 向资金池添加流动性

  • min_liquidity:用户期望获得最小的LP代币,如果铸造代币过少交易会回滚
  • max_tokens:用户提供的最大Token数量,如果计算出花费Token大于这个值交易会回滚
  • deadline:交易确认时间,交易超过该时限会回滚

代码逻辑:

  1. 检查时限是否超过
  2. 用户添加ETH和Token数量不能为零
  3. 添加流动性有两种情况:
    1. 添加流动性:通过添加流动性公式计算出花费Token的数量和能铸造出的LPT,如果花费Token数量大于max_tokens或能铸造出的LPT小于min_liquidity就需要回滚交易
    2. 添加初始流动性:池子最初价格有该笔交易决定,铸造的LPT与转移的ETH数量一致,添加的Token数量等于max_tokens
  4. 需要将token转移到合约地址(transferFrom)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function addLiquidity(
uint256 min_liquidity,
uint256 max_tokens,
uint256 deadline
) payable
external
returns (uint256)
{
require(deadline > block.timestamp, "block time exceeded deadline");
require(msg.value > 0 && max_tokens > 0, "add token or eth of amount can't equal zero");
uint256 total_liquidity = totalSupply();
if (total_liquidity > 0) {
require(min_liquidity > 0, "min_liquidity can't equal zero");
// eth_reserve = x, token_reserve = y
uint256 eth_reserve = address(this).balance - msg.value;
uint256 token_reserve = token.balanceOf(address(this));
// token_amount = dy = dx/x * y + 1
uint256 token_amount = msg.value * token_reserve / eth_reserve + 1;
// eth_amount = S = dx/x * T
uint256 liuiqity_minted = msg.value * total_liquidity / eth_reserve;
require(max_tokens > token_amount, "max_token does not meet the addLiquidity requirement");
require(liuiqity_minted > min_liquidity, "min_liquidity does not meet the addLiquidity requirement");
// mint liquidity
_mint(msg.sender, liuiqity_minted);
require(token.transferFrom(msg.sender, address(this), token_amount), "transferFrom failed");
emit AddLiquidity(msg.sender, msg.value, token_amount);
return liuiqity_minted;
} else {
require(msg.value >= 1000000000, "init_liquidity must input 1 ether");
require(factory.getExchange(address(token)) == address(this), "factory not create the pair");
uint256 token_amount = max_tokens;
// initial liquidity = x wei
uint256 initial_liquidity = address(this).balance;
// mint liquidity
_mint(msg.sender, initial_liquidity);
require(token.transferFrom(msg.sender, address(this), token_amount), "transferFrom failed");
emit AddLiquidity(msg.sender, msg.value, token_amount);
return initial_liquidity;
}
}

removeLiquidity(uint256 amount, uint256 min_eth, uint256 min_tokens, uint256 deadline)向资金池移除流动性

  • amount:想要销毁LPT数量
  • min_eth:用户最少能取出的ETH数量,如果能取出的最小ETH小于该值,交易会回滚
  • min_tokens:用户最少能取出的Token数量,如果能取出的最小Token小于该值,交易会回滚
  • deadline:交易确认时间,交易超过该时限会回滚

代码逻辑与添加流动性类似

  1. 检查输入参数是否满足条件,amount,min_eth,min_tokens大于零,当前时间没有时限。
  2. 计算给定销毁shares能够取出多少Token和ETH
  3. 检查取出的Token和Eth是否满足最小期望
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function removeLiquidity(
uint256 amount,
uint256 min_eth,
uint256 min_tokens,
uint256 deadline
) payable
external
returns (uint256 eth_amount, uint256 token_amount)
{
require(amount > 0, "removeLiquidity must greater than zero");
require(deadline > block.timestamp, "block time exceeded deadline");
require(min_eth > 0 && min_tokens > 0, "add token or eth of amount can't equal zero");
uint256 total_liquidity = this.totalSupply();
require(total_liquidity != 0, "total_liquidity equal zero");
uint256 token_reserve = token.balanceOf(address(this));
// eth_amount = dx = x * S / T
eth_amount = address(this).balance * amount / total_liquidity;
// token_amount = dy = y * S / T
token_amount = token_reserve * amount / total_liquidity;
require(eth_amount > min_eth && token_amount > min_tokens, "min_token or min_tokens does not meet the removeLiquidity requirement" );
// burn liquidity
_burn(msg.sender, amount);
payable(msg.sender).transfer(amount);
require(token.transfer(msg.sender, token_amount), "transferFrom failed");
emit RemoveLiquidity(msg.sender, eth_amount, token_amount);
}

价格查询

基础

价格计算

getInputPrice(uint256 input_amount, uint256 input_reserve, uint256 output_reserve) -> uint256

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function getInputPrice(
uint256 input_amount,
uint256 input_reserve,
uint256 output_reserve
) private
view
returns (uint256)
{
require(input_reserve > 0 && output_reserve > 0, "input or output equal zero");
// fee < 1000, not write require logic
uint256 input_amount_with_fee = 1 wei;
input_amount_with_fee = input_amount * (1000 - fee);
uint256 numrator = input_amount_with_fee * output_reserve;
uint256 denominator = ( input_reserve * 1000 ) + input_amount_with_fee;
// dy = y * dx * (1000-fee) / (x * 1000 + dx * (1000 -fee))
return numrator / denominator;

}

getOutputPrice(output_amount: uint256, input_reserve: uint256, output_reserve: uint256) -> uint256

1
2
3
4
5
6
7
8
9
10
11
12
13
function getOutputPrice(
uint256 output_amount,
uint256 input_reserve,
uint256 output_reserve
) private
pure
returns (uint256)
{
require(input_reserve > 0 && output_reserve > 0, "input or output equal zero");
uint256 numrator = input_reserve * output_amount * 1000;
uint256 denominator = (output_reserve - output_amount) * 997;
return numrator / denominator + 1;
}

以上面两个函数为基础得到计算eth与toekn之间的价格

getEthToTokenInputPrice(eth_sold: uint256(wei)) -> uint256: 输入要卖出的ETH数量,返回能得到的token数量

getEthToTokenOutputPrice(tokens_bought: uint256) -> uint256(wei):输入要买的token数量,返回需要给出的ETH数量

getTokenToEthInputPrice(tokens_sold: uint256) -> uint256(wei):输入要卖的token数量,返回得到的token数量

getTokenToEthOutputPrice(eth_bought: uint256(wei)) -> uint256输入得到的ETH数量,返回需要的token数量

兑换

exchange的其他函数包括有:ETH与Token的兑换、Token与ETH的兑换、Token与Token之间的兑换

代码通过内部ethToTokenInputethToTokenOutputtokenToEthOutput等函数兑换逻辑,一般先获得可以买/卖的代币/eth数量,确认能获得预期数量的token/eth,然后进行转账交易.

这里粗滤介绍下v1代码功能,因为v1使用的人太少加之v1是最早的版本,具体的操作和测试留到v2进行。关于代码具体细节可以看源码,或者重构的代码。

ETH与Token的兑换

  • ethToTokenSwapInput
  • ethToTokenTransferInput
  • ethToTokenOutput
  • ethToTokenSwapOutput

Token与ETH的兑换

  • tokenToEthSwapInput
  • tokenToEthTransferInput
  • tokenToEthSwapOutput
  • tokenToEthTransferOutput

Token与Token之间的兑换

  • tokenToTokenSwapInput
  • tokenToTokenTransferInput
  • tokenToTokenSwapOutput
  • tokenToTokenTransferOutput
  • tokenToExchangeSwapInput
  • tokenToExchangeTransferInput
  • tokenToExchangeSwapOutput
  • tokenToExchangeTransferOutput

Factory

factory通过createExchange创建exchange,一个代币创建一个exchange

1
2
3
4
5
6
7
8
9
10
11
function createExchange(address token) public returns(address) {
require(token != address(0));
require(token_to_exchange[token] == address(0));
Exchange exchange = new Exchange(token, 3);
token_to_exchange[token] = address(exchange);
exchange_to_token[address(exchange)] = token;
uint256 token_id = tokenCount + 1;
id_to_token[token_id] = token;
emit NewExchange(token, address(exchange));
return address(exchange);
}

参考资料

梁培利的个人空间-梁培利个人主页-哔哩哔哩视频 (bilibili.com)

Uniswap V1 原理与源码解析: https://juejin.cn/post/7172903744147980325

Uniswap源码仓库:https://github.com/Uniswap/v1-contracts

重构源码仓库地址:https://github.com/Salbt/Uniswap-v1-solidity