0%

Uniswap V1版本在2018年11月启动(创始人启动协议的Twitter)。Uniswap V1从本质上来说是一个去中心化的自动交易所,提供ETH和ERC-20代币之间进行兑换的功能。Uniswap V1协议的出现为基于Ethereum的DeFi生态发展奠定了基础。根据官网的文档所描述,Uniswap V1在设计之初注重去中心化、去监管化(censorship resistance)和安全性,是一个开源的公共产品(public good),不收取交易费用。

背景:ERC-20代币

下面首先简要介绍一下Ethereum中的代币。代币(Token)在Ethereum生态中可以代表一切可以量化的东西,比如货币(如对标美元的DAI代币和代表ETH本身的代币WETH)、股份(如向资金池投资所返回的资金池代币)等等。ERC-20代币标准是2015年在EIP-20中提出,作为Ethereum智能合约体系中代币实现的统一标准。代币的本身也是一种智能合约,定义了一系列行为,并记录了每个地址所拥有的代币数量。ERC-20代币接口的实现如下:

interface ERC20Interface {
    // 任何ERC-20代币必须实现如下接口
    // 查询代币的总供给量
    function totalSupply() public view returns (uint);
    // 查询某个地址所拥有的代币量
    function balanceOf(address tokenOwner) public view returns (uint balance);
    // 允许spender(从调用者这里)取走一定量的代币
    function approve(address spender, uint tokens) public returns (bool success);
    // 查询tokenOwner允许spender取走的代币数量
    function allowance(address tokenOwner, address spender) public view returns (uint remaining);
    // 将tokens数量的代币转移给地址to
    function transfer(address to, uint tokens) public returns (bool success);
    // 从地址from向地址to转账tokens数量的代币
    function transferFrom(address from, address to, uint tokens) public returns (bool success);
    
    // 以下功能是可选的
    function name() external view returns (string);
    function symbol() external view returns (string);
    function decimals() external view returns (string);

    // 转账过程可能触发的事件
    event Transfer(address indexed from, address indexed to, uint tokens);
    event Approval(address indexed tokenOwner, address indexed spender, uint tokens);
}

Uniswap V1体系概览

Uniswap V1本质上是一个自动的交易所,能够自动地和用户交互并兑换ETH和ERC-20代币,兑换比例的确定(即价格)采用恒定乘积自动做市系统(constant-product automated market maker),也就是说,交易所资金池内的ETH和ERC-20代币数量的乘积总体是恒定的。和传统交易所的OrderBook撮合机制不同,在自动做市机制中每个用户的交易对象(交易对手方)都是交易所本身,而交易所通过公式确定兑换比例。不同交易所(如去中心化的自动化交易所和Binance等传统的交易所)之间的价格一致性由套利机制进行实现:如果交易所之间的价格存在差异,那么就有投机者进行投机交易(如低买高卖)将价格差异缩小至套利机会不存在(即交易费用大于套利利润)。

V1版本的Uniswap协议采用Vyper语言进行开发(而非当前Ethereum合约开发主流的Solidity语言)。Vyper是一种语法和Python非常接近的语言。Ethereum智能合约发展初期,由于Solidity语法相对独特且发展较为缓慢,Vyper曾经是Ethereum合约开发的主流语言之一,随后才被快速发展且受到官方支持的Solidity取代。

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

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

Factory的实现

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,这是为了约束某个代币的所有流动性都划分在一个池子中,增加池子中对应的存储量,降低交易的滑点。

Exchange的实现

从Factory的代码可以看出,Uniswap的核心功能在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);
}

添加/取回流动性

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

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

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

\[Token_{Deposited}=Token_{Pool}*\frac{ETH_{Deposited}}{ETH_{Pool}}\]

\[ Amount_{LPToken}=TotalSupply_{LPToken}*\frac{ETH_{Deposited}}{ETH_{Pool}} \]

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

  • min_liquidity:用户期望的LP代币数量。如果最终产生的LP代币数量过少,则交易会回滚避免损失。
  • max_tokens:用户想要提供的最大代币量。如果计算得出的代币数量大于这个参数,代表用户不愿意提供更多代币,交易也会回滚。
  • deadline:时限。如果交易确认的区块时间大于deadline,也会回滚。
# 第一部分:total_liquidity > 0
@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

removeLiquidity(amount: uint256, min_eth: uint256(wei), min_tokens: uint256, deadline: timestamp) -> (uint256(wei), uint256)

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

价格查询

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

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

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

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

\[ Output=Reserve_{Output} * \frac{Input * 997}{Input * 997 + Reserve_{Input} * 1000} \]

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

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

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

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

\[ Input=Reserve_{Input} * \frac{Output * 1000}{997 * (Reserve_{Output} - Output)} + 1 \]

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

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

  • getEthToTokenInputPrice
  • getEthToTokenOutputPrice
  • getTokenToEthInputPrice
  • getTokenToEthOutputPrice

均在这两个函数的基础上进行实现。以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)

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

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

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

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

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

在交易机制上,Uniswap V1实现了两种交易方式: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)

参考:

Uniswap V1在Ethereum MainNet中的合约地址:

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

$ hexo new "My New Post"

More info: Writing

Run server

$ hexo server

More info: Server

Generate static files

$ hexo generate

More info: Generating

Deploy to remote sites

$ hexo deploy

More info: Deployment