02.uniswap_v1_code
2023-06-23 20:21:52 # 03.Uniswap

uniswap_v1_code

Uniswap V1是用Vyper言语进行开发的,Vyper是一种语法和Python非常挨近的言语。

V1的体系分为两个部分:

  • Exchange:用于进行ETH和ERC-20代币之间的兑换。
  • Factory:用于创立和记录一切的Exchange,也用于查询代币对应的Exchange。

Factory

  • initializeFactory:只在创建的时候被调用,一旦设置了template参数后就无法更改,确保了用于创建Exchange的代码模板不会被修改。template是链上部署的合约,用于作为后续创建的Exchange的模板。
  • createExchange:用于从模板创建一个Exchange。在做一些必要的校验之后,代码调用内置函数create_with_code_of拷贝exchangeTemplate所指示的地址中的代码创建一个新的合约并返回其地址。随后调用新创建的Exchange的setup函数设置代币地址,并将新创建的Exchange记录在合约内。注意到在做验证的过程中,函数约束每一个代币只能对应一个Exchange,这是为了约束某个代币的所有流动性都划分在一个池子中,增加池子中对应的存储量,降低交易的滑点。

Exchange

从Factory的代码可以看出,Uniswap的核心功能在Exchange中进行实现。Exchange的实现略有复杂,首先给出所有功能的接口,接下来按照每个接口的功能介绍其实现:

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
// 只保留了Exchange的核心功能接口
interface UniswapExchangeInterface {
// 流动性
function addLiquidity(uint256 min_liquidity, uint256 max_tokens, uint256 deadline) external payable returns (uint256);
function removeLiquidity(uint256 amount, uint256 min_eth, uint256 min_tokens, uint256 deadline) external returns (uint256, uint256);
// 价格查询
function getEthToTokenInputPrice(uint256 eth_sold) external view returns (uint256 tokens_bought);
function getEthToTokenOutputPrice(uint256 tokens_bought) external view returns (uint256 eth_sold);
function getTokenToEthInputPrice(uint256 tokens_sold) external view returns (uint256 eth_bought);
function getTokenToEthOutputPrice(uint256 eth_bought) external view returns (uint256 tokens_sold);
// 提供ETH以兑换代币
function ethToTokenSwapInput(uint256 min_tokens, uint256 deadline) external payable returns (uint256 tokens_bought);
function ethToTokenTransferInput(uint256 min_tokens, uint256 deadline, address recipient) external payable returns (uint256 tokens_bought);
function ethToTokenSwapOutput(uint256 tokens_bought, uint256 deadline) external payable returns (uint256 eth_sold);
function ethToTokenTransferOutput(uint256 tokens_bought, uint256 deadline, address recipient) external payable returns (uint256 eth_sold);
// 提供代币以兑换ETH
function tokenToEthSwapInput(uint256 tokens_sold, uint256 min_eth, uint256 deadline) external returns (uint256 eth_bought);
function tokenToEthTransferInput(uint256 tokens_sold, uint256 min_eth, uint256 deadline, address recipient) external returns (uint256 eth_bought);
function tokenToEthSwapOutput(uint256 eth_bought, uint256 max_tokens, uint256 deadline) external returns (uint256 tokens_sold);
function tokenToEthTransferOutput(uint256 eth_bought, uint256 max_tokens, uint256 deadline, address recipient) external returns (uint256 tokens_sold);
// 代币之间的互换
function tokenToTokenSwapInput(uint256 tokens_sold, uint256 min_tokens_bought, uint256 min_eth_bought, uint256 deadline, address token_addr) external returns (uint256 tokens_bought);
function tokenToTokenTransferInput(uint256 tokens_sold, uint256 min_tokens_bought, uint256 min_eth_bought, uint256 deadline, address recipient, address token_addr) external returns (uint256 tokens_bought);
function tokenToTokenSwapOutput(uint256 tokens_bought, uint256 max_tokens_sold, uint256 max_eth_sold, uint256 deadline, address token_addr) external returns (uint256 tokens_sold);
function tokenToTokenTransferOutput(uint256 tokens_bought, uint256 max_tokens_sold, uint256 max_eth_sold, uint256 deadline, address recipient, address token_addr) external returns (uint256 tokens_sold);
}

添加流动性

用户可以调用addLiquidityremoveLiquidity向资金池中添加和取回流动性。

1
addLiquidity(min_liquidity: uint256, max_tokens: uint256, deadline: timestamp) -> uint256

向资金池添加流动性。Uniswap V1中添加流动性的过程简述如下:

  1. 用户调用addLiquidity函数并发送一定量的ETH。
  2. Uniswap Exchange要求用户按照当前资金池中的ETH和代币的比例添加流动性。Uniswap通过比较发送的ETH量和池子中的ETH数量,计算用户需要发送的代币数量Token(deposit),并将该数量的代币从用户(交易的Sender)转给自己。
  3. 为了证明用户确实提供了流动性及用户流动性所占的份额,Uniswap Exchange将向用户发放LP(Liquidity Pool)代币,其数量为Amout(LPToken)。

由于Uniswap去中心化的特性,添加流动性的交易发出时和确认时流动性池的兑换比例(或者说价格)可能不同。为了避免这个问题给用户造成的损失,addLiquidity函数提供了三个参数进行控制:

  • min_liquidity:用户期望的LP代币数量。如果最终产生的LP代币数量过少,则交易会回滚避免损失。
  • max_tokens:用户想要提供的最大代币量。如果计算得出的代币数量大于这个参数,代表用户不愿意提供更多代币,交易也会回滚。
  • deadline:时限。如果交易确认的区块时间大于deadline,也会回滚。

去除流动性

提供者可以随时销毁他们的流动性代币,以从池中提取他们按比例提供的 ETH 和 ERC20 代币。ETH 和 ERC20 代币按当前汇率(reserve ratio)提取,而不是其原始投资的比率。这意味着一些价值可能会因市场波动和套利而损失。

价格查询

下面首先介绍Uniswap V1的价格机制。每个Exchange(或者说一个池子)中有且只有两种资产:ETH和代币,池子中两个资产存量(Reserve)的比率构成了价格,用户可以在ETH和代币以及代币和代币之间自由兑换。因此,用户有两种指定价格的方式:精确指定换出(Output)值,并限定最大的输入值(Input);或者精确指定换入(Input)值,并设置最小的输出值(Output)。

因此,Uniswap V1在实现中首先实现了两个私有函数作为定价体系:getInputPricegetOutputPrice

1
getInputPrice(input_amount: uint256, input_reserve: uint256, output_reserve: uint256)

在确定池子中输入单位和输出单位的存量时,精确的输入数量能换出的输出数量。不难看出,该函数实现了这样一个公式:

输入单位的0.3%作为交易费用,剩下的输入进入池子。因此分母为更新后池子中输入单位的存量,分子为除去交易费用后的输入,分式表达的是’ 输入进入池子后’ 输入在池子中所占的份额。该分式乘以输出池子的存量即为输入对应的输出量。

1
getOutputPrice(output_amount: uint256, input_reserve: uint256, output_reserve: uint256)

在确定池子中输入单位和输出单位的存量时,精确的计算出一定的输出数量能换出的输入数量。不难看出,该函数实现的公式是由上面的式子变换而来:

Uniswap提供价格查询的四个函数:

  • getEthToTokenInputPrice
  • getEthToTokenOutputPrice
  • getTokenToEthInputPrice
  • getTokenToEthOutputPrice

均在这两个函数的基础上进行实现。以getEthToTokenInputPrice为例:

1
2
3
4
def getEthToTokenInputPrice(eth_sold: uint256(wei)) -> uint256:
assert eth_sold > 0
token_reserve: uint256 = self.token.balanceOf(self)
return self.getInputPrice(as_unitless_number(eth_sold), as_unitless_number(self.balance), token_reserve)

可以看出,在实现时向getInputPrice中传入的存量参数{input,output}_reserve分别是Exchange本身的ETH数量和Exchange在代币合约中记录的代币存量。

兑换

eth=>token

有了价格计算函数,ETH和代币间的互换就变得非常直观。

通过精确的ETH输入量(eth_sold)计算价格并交换代币。通过getInputPrice计算输出的代币数量。同样包含了min_tokens最小代币输出量和deadline的时间限制。

1
2
3
4
5
6
7
8
9
10
11
12
@private
def ethToTokenInput(eth_sold: uint256(wei), min_tokens: uint256, deadline: timestamp, buyer: address, recipient: address) -> uint256:
# check
assert deadline >= block.timestamp and (eth_sold > 0 and min_tokens > 0)
# calculate output token
token_reserve: uint256 = self.token.balanceOf(self)
tokens_bought: uint256 = self.getInputPrice(as_unitless_number(eth_sold), as_unitless_number(self.balance - eth_sold), token_reserve)
assert tokens_bought >= min_tokens
# transfer token to recipient
assert self.token.transfer(recipient, tokens_bought)
log.TokenPurchase(buyer, eth_sold, tokens_bought)
return tokens_bought

通过精确的代币输出量(tokens_bought)计算价格并交换代币。通过getOutputPrice计算输入的ETH数量,并可能在ETH需求量小于用户发送量时产生Refund。同样包含了min_tokens最小代币输出量和deadline的时间限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@private
def ethToTokenOutput(tokens_bought: uint256, max_eth: uint256(wei), deadline: timestamp, buyer: address, recipient: address) -> uint256(wei):
# check
assert deadline >= block.timestamp and (tokens_bought > 0 and max_eth > 0)
# calculate input ETH
token_reserve: uint256 = self.token.balanceOf(self)
eth_sold: uint256 = self.getOutputPrice(tokens_bought, as_unitless_number(self.balance - max_eth), token_reserve)
# may have refund, also check (revert) if eth_sold > max_eth
eth_refund: uint256(wei) = max_eth - as_wei_value(eth_sold, 'wei')
if eth_refund > 0:
send(buyer, eth_refund)
# transfer token
assert self.token.transfer(recipient, tokens_bought)
log.TokenPurchase(buyer, as_wei_value(eth_sold, 'wei'), tokens_bought)
return as_wei_value(eth_sold, 'wei')

token=>ETH

tokenToEthInputtokenToEthOutput在实现上与上面两个函数基本一致。

token=>token

由于 ETH 被用作所有 ERC20 代币的公共对,因此它可以用作直接 ERC20 到 ERC20 交换的中介。例如,可以在一个交易所从 OMG 转换为 ETH,然后在一个交易中从 ETH 转换为 KNC。

要从 OMG 转换为 KNC(例如),买方调用tokenToTokenSwap()OMG 交换合约上的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
contract Factory():
def getExchange(token_addr: address) -> address: constant

contract Exchange():
def ethToTokenTransfer(recipent: address) -> bool: modifying

factory: Factory

@public
def tokenToTokenSwap(token_addr: address, tokens_sold: uint256):
exchange: address = self.factory.getExchange(token_addr)
fee: uint256 = tokens_sold / 500
invariant: uint256 = self.eth_pool * self.token_pool
new_token_pool: uint256 = self.token_pool + tokens_sold
new_eth_pool: uint256 = invariant / (new_token_pool - fee)
eth_out: uint256 = self.eth_pool - new_eth_pool
self.eth_pool = new_eth_pool
self.token_pool = new_token_pool
Exchange(exchange).ethToTokenTransfer(msg.sender, value=eth_out)

其中token_addr是 KNC 代币的地址,tokens_sold是出售的 OMG 数量。此函数首先检查工厂以检索 KNC 交换地址。接下来,交易所将输入的 OMG 转换为 ETH。然而,该函数并没有将购买的 ETH 返还给买家,而是调用了KNC 交易所的payable 函数:ethToTokenTransfer()

ethToTokenTransfer()接收 ETH 和买家地址,验证调用是从注册表中的交易所进行的,将 ETH 转换为 KNC,并将 KNC 转发给原始买家。ethToTokenTransfer()功能相同,ethToTokenSwap()但有额外的输入参数recipient: address。这用于将购买的代币转发给原始买家而不是msg.sender,在这种情况下将是 OMG 交易所。

swap&transfer

在交易机制上,Uniswap V1实现了两种交易方式:Swap和Transfer。两者的唯一差别在于,Swap调用的接收者固定为交易发送者(即msg.sender),而Transfer调用可以额外指定一个接收者。通过如下函数对可以清晰地看出:

1
2
3
4
5
6
7
8
9
10
11
12
@public
@payable
def ethToTokenSwapInput(min_tokens: uint256, deadline: timestamp) -> uint256:
# 'receipient' is msg.sender
return self.ethToTokenInput(msg.value, min_tokens, deadline, msg.sender, msg.sender)

@public
@payable
def ethToTokenTransferInput(min_tokens: uint256, deadline: timestamp, recipient: address) -> uint256:
assert recipient != self and recipient != ZERO_ADDRESS
# 'receipient' is specified as parameter
return self.ethToTokenInput(msg.value, min_tokens, deadline, msg.sender, recipient)
Prev
2023-06-23 20:21:52 # 03.Uniswap
Next