Uniswap V1 详解

admin
admin 2024年09月21日
  • 在其它设备中阅读本文章

Uniswap V1 本质是一个去中心化的自动交易所,提供 ETH 和任意 ERC20 代币之间相互兑换的功能。特点是去中心化、去监管化和安全性,是一个开源的公共产品,不收取交易费用。

概览

Uniswap 采用恒定乘积自动做市系统,即资金池内的 ETH 和 ERC20 代币数量的乘积总体是恒定的。与传统交易所基于订单簿并促进买卖双方的代币交易方式不同,Uniswap 是基于各种代币的流动性储备来完成代币兑换交易的,这样的好处是买卖双方能快速直接地完成代币兑换而不必挂单等待。

代币兑换的价格由恒定乘积做市场来决定(x*y=k),这种自动做市的机制能使得代币的总体储备相对均衡。代币的储备由流动性提供者保障,流动性提供者会按代币交易费用的一定比例获取奖励。Uniswap 的一个重要特征是利用工厂 / 注册(factory/registry)合约,为每个 ERC20 代币部署一个单独的代币兑换合约,并且这些交易合约各自持有 ETH 及其相关的 ERC20 代币来用于代币兑换。 代币兑换合约通过注册表链接,因此可以以 ETH 作为中介直接进行 ERC20 代币到 ERC20 代币的兑换交易。

Uniswap V1 的体系相对来说比较简单,分为两个合约:

  1. Exchange,用于进行 ETH 和 ERC-20 代币之间的兑换。
  2. Factory,用于创建和记录所有的 Exchange,也用于查询代币对应的 Exchange。

Factory 合约

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

NewExchange: event({token: indexed(address), exchange: indexed(address)})

exchangeTemplate: public(address)
tokenCount: public(uint256)
token_to_exchange: address[address]
exchange_to_token: address[address]
id_to_token: address[uint256]

@public
def initializeFactory(template: address):
    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)
    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

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

@public
@constant
def getToken(exchange: address) -> address:
    return self.exchange_to_token[exchange]

@public
@constant
def getTokenWithId(token_id: uint256) -> address:
    return self.id_to_token[token_id]
  • initializeFactory 只在创建的时候被调用,一旦设置了 template 参数后就无法更改,确保了用于创建 Exchange 的代码模板不会被修改。template 是链上部署的合约,用于作为后续创建的 Exchange 的模板。
  • createExchange 用于从模板创建一个 Exchange。在做一些必要的校验之后,代码调用内置函数 create_with_code_of 拷贝 exchangeTemplate 所指示的地址中的代码创建一个新的合约并返回其地址。随后调用新创建的 Exchange 的 setup 函数设置代币地址,并将新创建的 Exchange 记录在合约内。注意到在做验证的过程中,函数约束每一个代币只能对应一个 Exchange,这是为了约束某个代币的所有流动性都划分在一个池子中,增加池子中对应的存储量,降低交易的滑点。
  • 所有的代币兑换合约地址都存储在工厂合约中,并且 ERC20 代币地址和兑换合约地址之间可以相互查询。getExchange() 函数可以根据传入的代币地址找到对应的兑换合约地址,getToken() 函数可以根据传入的兑换合约地址找到相应的 ERC20 代币地址。

Exchange 合约

Exchange 的实现略有复杂,下面是其接口定义

// 只保留了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);
}

添加 / 移除流动性

用户可以调用 addLiquidity 和 removeLiquidity 向资金池中添加和取回流动性。添加流动性的过程简述如下:

  1. 用户调用 addLiquidity 函数并存入(发送)一定数量的 ETH
  2. Exchange 合约按照当前兑换比例计算用户需要支付的代币数量,并将该数量的代币从用户转给自己
  3. 为了证明用户确实提供了流动性及用户流动性所占的份额,Exchange 将向用户发放 LP(Liquidity Pool)代币
存入Token数量 = Token总量 * 存入ETH数量 / ETH总量
发放LP数量 = LP总量 * 存入ETH数量 / ETH总量

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

  1. min_liquidity:用户期望的 LP 代币数量。如果最终产生的 LP 代币数量过少,则交易回滚
  2. max_tokens:用户想要提供的最大代币量。如果计算得出的代币数量大于这个参数,则交易回滚
  3. deadline:时限。如果交易确认的区块时间大于 deadline,则交易回滚
@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 = totalSupply of LP token
    total_liquidity: uint256 = self.totalSupply

    if total_liquidity > 0:
        assert min_liquidity > 0
        # eth & token reserve in the pool
        eth_reserve: uint256(wei) = self.balance - msg.value
        token_reserve: uint256 = self.token.balanceOf(self)
        # the amount of token user should also provide 
        token_amount: uint256 = msg.value * token_reserve / eth_reserve + 1
        # minted amount of LP token
        liquidity_minted: uint256 = msg.value * total_liquidity / eth_reserve
        assert max_tokens >= token_amount and liquidity_minted >= min_liquidity
        # record LP token balance & totalSupply
        self.balances[msg.sender] += liquidity_minted
        self.totalSupply = total_liquidity + liquidity_minted
        # transfer tokens from user to Exchange
        assert self.token.transferFrom(msg.sender, self, token_amount)

        log.AddLiquidity(msg.sender, msg.value, token_amount)
        log.Transfer(ZERO_ADDRESS, msg.sender, liquidity_minted)
        return liquidity_minted

流动性添加者可以随时通过销毁他们的流动性代币,从代币兑换池中按他们所持流动性代币所占比例提取相同比例的 ETH 和 ERC20 代币。所能提取的 ETH 和 ERC20 代币计算公式如下:

取回ETH数量 = ETH总量 * 要销毁的LP数量 / LP总量
取回Token数量 = Token总量 * 要销毁的LP数量 / LP总量

ETH 和 ERC20 代币按照代币兑换池当前的价格(比例)提取,而不是流动性提供者提供流动性时的价格,因此流动性提供者在移除流动性时可能会因为市场波动而蒙受损失。

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 = totalSupply of LP token
    total_liquidity: uint256 = self.totalSupply
    assert total_liquidity > 0
    token_reserve: uint256 = self.token.balanceOf(self)
    # calculate returned eth & token amount
    eth_amount: uint256(wei) = amount * self.balance / total_liquidity
    token_amount: uint256 = amount * token_reserve / total_liquidity
    # check
    assert eth_amount >= min_eth and token_amount >= min_tokens
    # update status
    self.balances[msg.sender] -= amount
    self.totalSupply = total_liquidity - amount
    # send eth & token back
    send(msg.sender, eth_amount)
    assert self.token.transfer(msg.sender, token_amount)
    # log event
    log.RemoveLiquidity(msg.sender, eth_amount, token_amount)
    log.Transfer(msg.sender, ZERO_ADDRESS, amount)
    return eth_amount, token_amount

价格计算

每个 Exchange(或者说一个池子)中有且只有两种资产:ETH 和代币,池子中两个资产存量(Reserve)的比率构成了兑换比例(价格)。因此,用户有两种指定价格的方式:精确指定换出(Output)值,并限定最大的输入值(Input);或者精确指定换入(Input)值,并设置最小的输出值(Output)。另外:Exchange 会收取 0.3% 的手续费(在输入端收取,剩下的部分按实际价格兑换)奖励给所有流动性提供者。因此,Uniswap 实现了两个私有函数作为定价体系:getInputPrice 和 getOutputPrice。

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

def getOutputPrice(output_amount: uint256, input_reserve: uint256, output_reserve: uint256) -> uint256:
    assert input_reserve > 0 and output_reserve > 0
    numerator: uint256 = input_reserve * output_amount * 1000
    denominator: uint256 = (output_reserve - output_amount) * 997
    return numerator / denominator + 1

实际返回的不是价格,而是可以获得资金的数量,并且包括了 0.3% 手续费,获取输出数量公式根据恒定乘积公式和手续费推导,再进行公式变换可获得输入数量公式,具体计算公式如下:

输出数量 = 输出总量 * (输入数量*997 / (输入数量*997 + 输入总量*1000))
输入数量 = 输入总量 * (输出数量*1000 / (输出总量 - 输出数量)* 997) + 1

在确定池子中输入单位和输出单位的存量时,精确的输入数量能换出的输出数量;在确定池子中输入单位和输出单位的存量时,精确的输出数量能换出的输入数量。Uniswap 提供价格查询的四个函数:

  • getEthToTokenInputPrice
  • getEthToTokenOutputPrice
  • getTokenToEthInputPrice
  • getTokenToEthOutputPrice

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

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)

ETH 和代币间的互换

有了价格计算函数,ETH 和代币间的互换就变得非常直观。首先来看内部实现的四个函数:
通过精确的 ETH 输入量(eth_sold)计算价格并交换代币。通过 getInputPrice 计算输出的代币数量。包含了 min_tokens 最小代币输出量和 deadline 的时间限制。

@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

tokenToEthInput 和 tokenToEthOutput 两个函数在实现上面基本一致, 主要区别在于通过 getOutputPrice 计算输入的 ETH 数量。

@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')

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

@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)

代币和代币间的互换

由于 ETH 被用作所有代币的公共对,它可以被用来作为代币与代币之间兑换的桥梁。比如如果想要用 ABC 代币兑换 XYZ,则可以先用 ABC 兑换成 ETH,然后再用 ETH 兑换成 XYZ。

手续费

ETH 兑换 ERC20 代币

  • 用 0.3% 的 ETH 支付手续费
    ERC20 代币兑换 ETH
  • 用 0.3% 的 ERC20 代币支付手续费
    ERC20 代币兑换 ERC20 代币
  • 用 0.3% 的 ERC20 代币支付从 ERC20 到 ETH 的手续费
  • 用 0.3% 的 ETH 代币支付从 ETH 到 ERC20 的手续费(相对于扣除完第一次手续费之后的 0.3%)
  • 最终的手续费比率为 0.5991%
    ERC20 代币到 ERC20 代币的交易包括 ERC20 代币到 ETH 和 ETH 到 ERC20 代币两笔兑换,因此手续费是分别支付给两个兑换合约的。Uniwap 没有收取额外的平台费用。手续费会直接被加入池子且没有额外铸造流动性代币,这相当于增加了所有流动性代币的价值,因此流动性提供者会因为用户在该池子进行代币兑换而获益,这部分收益最终可以通过销毁流动性代币来获得。

Uniswap V1 在 Ethereum Mainnet 中的合约详情: