The Way to Uniswap

中心化交易所通常采用「订单薄」(Order Book)模式撮合交易,其中存在「做市商」(Market Maker)这一角色,它起到增加交易所订单「深度」的作用。

区块链 TPS 远低于中心化交易所,且交易手续费不菲,再这样的一种新兴平台上传统做市商很难高频地调整买单和卖单以提供做市服务。

  • 传统做市商具体如何赚钱?

区块链生态中与传统做市商对应的是「自动做市商」(Automated Market Maker, AMM),这是一种去中心化交易所(Decentralized Exchange, DEX),通常采用一个简洁的「恒定乘积方程」(constant product formula, $$x * y = k$$)作为数学模型为资产定价。用户可以创建新的流动性池(Liquid Pool)或者为已有的流动性池提供流动性,这样的用户就成了流动性提供者(Liquid Providers, LPs)。

  • 用户执行兑换交易的交易对象是流动性池(不是其它交易者),每个流动性池由两种资产组成。
  • AMM 会对兑换交易收取手续费,用户为流动性池提供流动性的动机就是赚取这部分手续费。

恒定乘积方程

恒定乘积方程 $$x * y = k$$ 中:

  • x 和 y 是两种资产的数量,不论 x 和 y 如何变化,最终要模型维持 x 和 y 的乘积恒定。
  • 不考虑兑换手续费时,x 和 y 的兑换公式是 $$∆y = (∆x * y) / (x + ∆x)$$,对于深度足够的流动性池来说可以忽略分母中 $$+ ∆x$$ 部分,公式可以简化为 $$∆y = (∆x * y) / x$$,即 $$\frac{∆y}{∆x} = \frac{y}{x}$$。这意味着 x 和 y 组成的交易对的边际价格可以由流动性池中 x 和 y 的数量的比率来反映。
    • 假设 $$x = 10, y = 1000$$,则兑换 1 个 x 约能得到 100 个 y,即 x 的价格约为 y 的 100 倍。
    • A liquidity pool determines the price of its assets from the assets’ ratio in the pool.
  • 这一模型假设了流动性池中两种资产的兑换率与市场价出现偏差时,套利者(arbitrage traders)会通过 Swap 操作来搬平:从流动性池换走在其中价格被低估的资产、换来在其中价格被高估的资产,直到两者的兑换率重新回归市场价。

无常损失

无常损失」(Impermanent Loss)的定义是相较于只是持有资产,用户用这些资产为流动性池提供流动性且因为资产的价格发生波动而带来的资产缩水。

  • 只要流动性池中的资产发生价格波动(无论上涨还是下跌),就会产生无常损失,且价格波动越大无常损失也越大。
    • The only thing impermanent loss cares about is the price ratio relative to the time of deposit.
  • 场景分析(不考虑手续费和 DAI 的价格波动):由 ETH/DAI 组成的流动性池,市场价 1 DAI 等于 1 USD,1 ETH 等于 100 USD,此时 Alice 按照流动性池对应的 exchange 合约计算出的比率存入 1 ETH 和 100 DAI 为其提供流动性(总价值 200 USD);在 Alice 存入后,流动性池包含 10 ETH 和 1000 DAI,其中 Alice 占了 10% 的份额;恒定乘积等于 10000。
    1. ETH 市场价从 100 USD 涨至 400 ETH;
      • 1 ETH 的市场价有 400 DAI,而流动性池中 1 ETH 只有 100 DAI,流动性池中的 ETH 的价格被低估,成为套利的目标(被换走)。
      • 套利者用 DAI 来 Swap 流动性池中所有价格低于 400 USD 的 ETH,直到流动性池中的 ETH 价格也是 400 USD。
      • 套利者搬平兑换率差后,流动性池中的 ETH 的价格是 DAI 的 400 倍,也就是说 DAI 的数量是 ETH 的 400 倍,根据恒定乘积方程可以算出此时 ETH 数量是 5,DAI 数量是 2000。
      • 假设 Alice 移除流动性时,她可以拿到 $$5 * 10\% = 0.5$$ 个 ETH 和 $$2000 * 10\% = 200$$ 个 DAI,总价值 400 USD;若 Alice 没有提供流动性池,此时她的 1 ETH 和 100 DAI 的总价值 500 USD,中间相差的 100 USD 就是无常损失(20%)。
    2. ETH 市场价从 100 USD 跌至 25 USD(流动性池中 )。
      • 1 ETH 的市场价只有 25 DAI,而流动性池中 1 ETH 等于 100 DAI,流动性池中的 DAI 的价格被低估,成为套利的目标(被换走)。
      • 套利者用 ETH 来兑换 DAI,套利的结果是 ETH 数量是 20,DAI 数量是 500。
      • 假设 Alice 移除流动性,它可以拿到 2 ETH 和 50 DAI,总价值 100 USD;若 Alice 没有提供流动性,此时她的 1 ETH 和 100 DAI 的总价值是 125 USD,中间相差的 25 USD 就是无常损失(20%)。
    • 如果把作为 LPs 赚取的手续费考虑在内,一定程度上可以补偿无常损失。
  • 无常损失是可逆的,即非永久的,只有当出现无常损失且用户移除了流动性,无常损失才变成确实的损失。

滑点

滑点(slippage):因流动性不足造成的实际成交价格和期望成交价格之间的不理想的价差。

出现滑点的主要原因有:

  1. 交易所缺乏流动性;
  2. 市场上资产价格波动大。

V1 白皮书

Uniswap 是典型的基于恒定乘积模型的 AMM。

  • 用户提供流动性后,组成该流动性的两种资产被锁定到相关的合约中,成为 LP 的用户获得相应比例的 UNI token 作为提供流动性的凭证
    • 这些 UNI token 被铸造出来,发放到 LP 的地址。
    • 获得的 UNI token 的数量在当下已发放了的所有 UNI token 的数量里的占比和当下该用户提供的资产在流动性池中的资产总数里的占比相同。
  • 用户用 UNI token 移除流动性时,这部分的 UNI token 被燃烧掉,换回对应的两种资产。
    • 可以赎回的两种资产在流动性池中的占比和当下这部分 UNI token 在 UNI token 的总发放量里的占比相同。
  • 每执行一次 Swap,增加的资产的数量是足额的,由于手续费的存在,因兑换而减少的另一种资产实际减少的数量却略少一些,结果是“恒定”乘积每次 Swap 后都会增大一点

要点:

  • V1 中协议没有抽取手续费,所有手续费(0.3%)收入都归属流动性提供者。
  • 只有 ETH 和 ERC20 token 能组成流动性池;ERC20 之间的交换需要以 ETH 作为中介(收两次手续费)。
  • 每个 exchange 合约实现了 ERC20 接口,代币是 UNI-V1,持有者和持有数量的关系通过 self.balances 这一 map 来维护;self.token 保存了在此 exchange 上交易的 ERC20 token 的合约地址。

V1 core 代码分析

创建 ERC20/ETH 交易对合约

V1 用工厂合约为每个 ERC20 token 和 ETH 组成的交易对部署一个独立的合约(exchange contract)。

contract Exchange():
    def setup(token_addr: address): modifying

exchangeTemplate: public(address)       # 创建一个新交易所所需的模板合约的地址
token_to_exchange: address[address]
exchange_to_token: address[address]

# 此交易对合约发行的 UNI token 信息
name: public(bytes32)                   # Uniswap V1    (0X556e6973776170205631)
symbol: public(bytes32)                 # UNI-V1        (0X554e492d5631)
decimals: public(uint256)               # 18
totalSupply: public(uint256)            # total number of UNI in existence
balances: uint256[address]              # UNI balance of an address
allowances: (uint256[address])[address] # UNI allowance of one address on another

token: address(ERC20)                   # address of the ERC20 token traded on this contract

factory: Factory                        # interface for the factory that created this contract

@public
def initializeFactory(template: address):
    # ZERO_ADDRESS: 0x0000000000000000000000000000000000000000 
    # https://docs.vyperlang.org/en/v0.2.9/constants-and-vars.html#built-in-constants
    assert self.exchangeTemplate == ZERO_ADDRESS
    assert template != ZERO_ADDRESS
    self.exchangeTemplate = template

@public
def createExchange(token: address) -> address:
    assert token != ZERO_ADDRESS
    assert self.exchangeTemplate != ZERO_ADDRESS
    assert self.token_to_exchange[token] == ZERO_ADDRESS # 判重

    exchange: address = create_with_code_of(self.exchangeTemplate)
    Exchange(exchange).setup(token) # 起到新建的 exchange 合约的构造器的作用

    self.token_to_exchange[token] = exchange
    self.exchange_to_token[exchange] = token
    token_id: uint256 = self.tokenCount + 1
    self.tokenCount = token_id
    self.id_to_token[token_id] = token
    log.NewExchange(token, exchange)
    return exchange

# @dev This function acts as a contract constructor which is not currently supported in contracts deployed
#      using create_with_code_of(). It is called once by the factory during contract creation.
@public
def setup(token_addr: address):
    assert (self.factory == ZERO_ADDRESS and self.token == ZERO_ADDRESS) and token_addr != ZERO_ADDRESS
    self.factory = msg.sender
    self.token = token_addr

    self.name = 0x556e697377617020563100000000000000000000000000000000000000000000
    self.symbol = 0x554e492d56310000000000000000000000000000000000000000000000000000
    self.decimals = 18

# https://docs.vyperlang.org/en/v0.1.0-beta.8/built-in-functions.html#create-with-code-of
def create_with_code_of(a, value=b):
  """
  :param a: the address of the contract to duplicate.
  :type a: address
  :param b: the wei value to send to the new contract instance (Optional, default: 0)
  :type b: uint256(wei)
  """
  • 一种 ERC20 token 只能创建一个 exchange。
  • 每个 exchange 都实现了 ERC20 接口和事件(totalSupply, balanceOf, transfer, transferFrom, approve, allowance, Transfer, Approval),token 是 UNI-V1

ETH 换 ERC20 token

# @notice Convert ETH to Tokens.
# @dev User specifies exact input (msg.value) and minimum output.
# @param min_tokens Minimum Tokens bought.
# @param deadline Time after which this transaction can no longer be executed.
# @return Amount of Tokens bought.
@public
@payable
def ethToTokenSwapInput(min_tokens: uint256, deadline: timestamp) -> uint256:
    return self.ethToTokenInput(msg.value, min_tokens, deadline, msg.sender, msg.sender)

@private
def ethToTokenInput(eth_sold: uint256(wei), min_tokens: uint256, deadline: timestamp, buyer: address, recipient: address) -> uint256:
    assert deadline >= block.timestamp and (eth_sold > 0 and min_tokens > 0)
    token_reserve: uint256 = self.token.balanceOf(self) # ERC2 token 余额
    # https://docs.vyperlang.org/en/v0.1.0-beta.15/built-in-functions.html#as_unitless_number
    # ! 此处的 self.balance 已经是加上本次 msg.value(也就是 eth_sold)后的值了;self.balance 同时也包含了以 ETH 的形式收取的 0.3% 的兑换手续费
    tokens_bought: uint256 = self.getInputPrice(as_unitless_number(eth_sold), as_unitless_number(self.balance - eth_sold), token_reserve)
    assert tokens_bought >= min_tokens
    assert self.token.transfer(recipient, tokens_bought) # 以 exchange 合约的身份将 ERC20 token 转给兑换者
    log.TokenPurchase(buyer, eth_sold, tokens_bought)
    return tokens_bought

# @dev Pricing function for converting between ETH and Tokens.
# @param input_amount Amount of ETH or Tokens being sold.
# @param input_reserve Amount of ETH or Tokens (currency of input type) in exchange reserves.
# @param output_reserve Amount of ETH or Tokens (currency of output type) in exchange reserves.
# @return Amount of ETH or Tokens bought.
@private
@constant
def getInputPrice(input_amount: uint256, input_reserve: uint256, output_reserve: uint256) -> uint256:
    assert input_reserve > 0 and output_reserve > 0
    input_amount_with_fee: uint256 = input_amount * 997
    numerator: uint256 = input_amount_with_fee * output_reserve
    denominator: uint256 = (input_reserve * 1000) + input_amount_with_fee
    return numerator / denominator
  • 计算公式:
    1. $$ethAmountBeforeSwap × erc20TokenAmountBeforeSwap = invariant$$
    2. $$erc20TokenAmountBeforeSwap - \frac{ ethAmountBeforeSwap × erc20TokenAmountBeforeSwap }{ ethAmountBeforeSwap + ethAmountToSwap × 0.997 } = \frac{ethAmountToSwap × 0.997 × erc20TokenAmountBeforeSwap}{ethAmountBeforeSwap + ethAmountToSwap × 0.997} = \frac{ethAmountToSwap × 997 × erc20TokenAmountBeforeSwap}{ethAmountBeforeSwap × 1000 + ethAmountToSwap × 997}$$
    • Formalized Model 中的计算公式 $$∆y = (997 * ∆x * y) / (1000 * x + 997 * ∆x)$$ 一致。
      1. $$10_{ETH} * 500_{OMG} = 5000$$
      2. $$1 * 997.5 * 500 / (10 * 1000 + 1 * 997.5) = 45.35$$
      • $$11_{ETH} * 454.65_{OMG} = 5001.15$$
    • 0.3% 的 ETH 作为手续费自动进到了 self.balance 中,成了 LPs 共享的收益。

合约的逻辑使得以下方程得以满足:

$$(x_1 - 0.003 \cdot x_{in}) \cdot y_1 >= x_0 \cdot y_0$$

  • 这里描述的的 $$x$$ 兑换 $$y$$ 的;$$x_1$$ 中包含了 $$x_{in}$$。

ERC20 token 换 ETH

# @notice Convert Tokens to ETH.
# @dev User specifies exact input and minimum output.
# @param tokens_sold Amount of Tokens sold.
# @param min_eth Minimum ETH purchased.
# @param deadline Time after which this transaction can no longer be executed.
# @return Amount of ETH bought.
@public
def tokenToEthSwapInput(tokens_sold: uint256, min_eth: uint256(wei), deadline: timestamp) -> uint256(wei):
    return self.tokenToEthInput(tokens_sold, min_eth, deadline, msg.sender, msg.sender)

@private
def tokenToEthInput(tokens_sold: uint256, min_eth: uint256(wei), deadline: timestamp, buyer: address, recipient: address) -> uint256(wei):
    assert deadline >= block.timestamp and (tokens_sold > 0 and min_eth > 0)
    token_reserve: uint256 = self.token.balanceOf(self)
    eth_bought: uint256 = self.getInputPrice(tokens_sold, token_reserve, as_unitless_number(self.balance))
    wei_bought: uint256(wei) = as_wei_value(eth_bought, 'wei')
    assert wei_bought >= min_eth
    send(recipient, wei_bought)
    assert self.token.transferFrom(buyer, self, tokens_sold) # 将兑换者的 ERC20 token 转给此 exchange 合约的地址;0.3% 的手续费以 ERC20 token 的形式包含在内
    log.EthPurchase(buyer, tokens_sold, wei_bought)
    return wei_bought
  • tokens_sold 的 99.7% 被兑换成 ETH,但 100% 的 tokens_sold 都进入了 self.token.balanceOf(self),多出的 0.3% 的 ERC20 token 是由 LPs 共享的收益。

ERC20 token 互换

# @notice Convert Tokens (self.token) to Tokens (token_addr).
# @dev User specifies exact input and minimum output.
# @param tokens_sold Amount of Tokens sold.
# @param min_tokens_bought Minimum Tokens (token_addr) purchased.
# @param min_eth_bought Minimum ETH purchased as intermediary.
# @param deadline Time after which this transaction can no longer be executed.
# @param token_addr The address of the token being purchased.
# @return Amount of Tokens (token_addr) bought.
@public
def tokenToTokenSwapInput(tokens_sold: uint256, min_tokens_bought: uint256, min_eth_bought: uint256(wei), deadline: timestamp, token_addr: address) -> uint256:
    exchange_addr: address = self.factory.getExchange(token_addr) # 找到要兑换得到的 ERC20 token 和 ETH 组成的 exchange 的地址
    return self.tokenToTokenInput(tokens_sold, min_tokens_bought, min_eth_bought, deadline, msg.sender, msg.sender, exchange_addr)

@private
def tokenToTokenInput(tokens_sold: uint256, min_tokens_bought: uint256, min_eth_bought: uint256(wei), deadline: timestamp, buyer: address, recipient: address, exchange_addr: address) -> uint256:
    assert (deadline >= block.timestamp and tokens_sold > 0) and (min_tokens_bought > 0 and min_eth_bought > 0)
    assert exchange_addr != self and exchange_addr != ZERO_ADDRESS
    token_reserve: uint256 = self.token.balanceOf(self)
    eth_bought: uint256 = self.getInputPrice(tokens_sold, token_reserve, as_unitless_number(self.balance)) # 第一次收取手续费
    wei_bought: uint256(wei) = as_wei_value(eth_bought, 'wei')
    assert wei_bought >= min_eth_bought
    assert self.token.transferFrom(buyer, self, tokens_sold)

    tokens_bought: uint256 = Exchange(exchange_addr).ethToTokenTransferInput(min_tokens_bought, deadline, recipient, value=wei_bought) # 第二次收手续费
    log.EthPurchase(buyer, tokens_sold, wei_bought)
    return tokens_bought

# @notice Convert ETH to Tokens and transfers Tokens to recipient.
# @dev User specifies exact input (msg.value) and minimum output
# @param min_tokens Minimum Tokens bought.
# @param deadline Time after which this transaction can no longer be executed.
# @param recipient The address that receives output Tokens.
# @return Amount of Tokens bought.
@public
@payable
def ethToTokenTransferInput(min_tokens: uint256, deadline: timestamp, recipient: address) -> uint256:
    assert recipient != self and recipient != ZERO_ADDRESS
    return self.ethToTokenInput(msg.value, min_tokens, deadline, msg.sender, recipient)
  • 向 ETH-tokenA 的 exchange 发起 tokenA 兑换 tokenB 的交易。
  • 调用了两次 getInputPrice,收两次手续费。

提供流动性

# @notice Deposit ETH and Tokens (self.token) at current ratio to mint UNI tokens.
# @dev min_liquidity does nothing when total UNI supply is 0.
# @param min_liquidity Minimum number of UNI sender will mint if total UNI supply is greater than 0.
# @param max_tokens Maximum number of tokens deposited. Deposits max amount if total UNI supply is 0.
# @param deadline Time after which this transaction can no longer be executed.
# @return The amount of UNI minted.
@public
@payable
def addLiquidity(min_liquidity: uint256, max_tokens: uint256, deadline: timestamp) -> uint256:
    assert deadline > block.timestamp and (max_tokens > 0 and msg.value > 0)
    total_liquidity: uint256 = self.totalSupply # total number of UNI in existence
    if total_liquidity > 0:
        assert min_liquidity > 0
        eth_reserve: uint256(wei) = self.balance - msg.value    # 流动性池在本次提供流动性之前的 ETH 存量(ETH 已入池,数量是 msg.value)
        token_reserve: uint256 = self.token.balanceOf(self)     # 流动性池在本次提供流动性之前的 ERC20 token 存量(ERC20 token 未入池)
        # 以本次存入的 ETH 数量在 ETH 存量中的比例作为系数,计算需要存入的 ERC20 token 数量和 UNI 的铸币量
        token_amount: uint256 = msg.value * token_reserve / eth_reserve + 1 # 向上取整(没考虑恰好整除的情况)
        liquidity_minted: uint256 = msg.value * total_liquidity / eth_reserve
        assert max_tokens >= token_amount and liquidity_minted >= min_liquidity
        self.balances[msg.sender] += liquidity_minted # LP 获得相应数量的 UNI token
        self.totalSupply = total_liquidity + liquidity_minted
        assert self.token.transferFrom(msg.sender, self, token_amount) # ERC20 token 入池
        log.AddLiquidity(msg.sender, msg.value, token_amount)
        log.Transfer(ZERO_ADDRESS, msg.sender, liquidity_minted)
        return liquidity_minted
    else:
        # 至少需要存入 10 个 ETH
        assert (self.factory != ZERO_ADDRESS and self.token != ZERO_ADDRESS) and msg.value >= 1000000000
        # self 是流动性池对应的 exchange 合约
        assert self.factory.getExchange(self.token) == self
        token_amount: uint256 = max_tokens
        # ETH 已入池,数量是 msg.value
        initial_liquidity: uint256 = as_unitless_number(self.balance) # ! self.balance 包含了本次交易的 msg.value,self.balances >= msg.value
        # UNI 的总供应量(也就是本次的铸币量)设置成 self.balance 的值
        self.totalSupply = initial_liquidity 
        # LP 获得的 UNI token 数量也是 self.balance  
        self.balances[msg.sender] = initial_liquidity # 此代码分支的 totalSupply,每个 self.balances 中的每个地址对应的 UNI 个数也必然是 0,所以用了 = 而没有用 +=
        # 入池的 ERC20 token 数量就是入参 max_tokens
        assert self.token.transferFrom(msg.sender, self, token_amount)
        log.AddLiquidity(msg.sender, msg.value, token_amount) 
        log.Transfer(ZERO_ADDRESS, msg.sender, initial_liquidity)
        return initial_liquidity

@public
@constant
def getExchange(token: address) -> address:
    return self.token_to_exchange[token]

移除流动性

# @dev Burn UNI tokens to withdraw ETH and Tokens at current ratio.
# @param amount Amount of UNI burned.
# @param min_eth Minimum ETH withdrawn.
# @param min_tokens Minimum Tokens withdrawn.
# @param deadline Time after which this transaction can no longer be executed.
# @return The amount of ETH and Tokens withdrawn.
@public
def removeLiquidity(amount: uint256, min_eth: uint256(wei), min_tokens: uint256, deadline: timestamp) -> (uint256(wei), uint256):
    assert (amount > 0 and deadline > block.timestamp) and (min_eth > 0 and min_tokens > 0)
    total_liquidity: uint256 = self.totalSupply
    assert total_liquidity > 0
    token_reserve: uint256 = self.token.balanceOf(self)
    # 比率是 amount / total_liquidity
    eth_amount: uint256(wei) = amount * self.balance / total_liquidity
    token_amount: uint256 = amount * token_reserve / total_liquidity

    assert eth_amount >= min_eth and token_amount >= min_tokens
    self.balances[msg.sender] -= amount
    self.totalSupply = total_liquidity - amount # burn UNI token
    send(msg.sender, eth_amount)
    assert self.token.transfer(msg.sender, token_amount) # 以 exchange 合约的身份将 ERC20 转回赎回者
    log.RemoveLiquidity(msg.sender, eth_amount, token_amount)
    log.Transfer(msg.sender, ZERO_ADDRESS, amount)
    return eth_amount, token_amount

References