Uniswap V1 白皮书
原文地址:https://hackmd.io/@HaydenAdams/HJ9jLsfTz#%F0%9F%A6%84-Uniswap-Whitepaper
介绍
Uniswap 是一个基于以太坊的自动化代币交换的协议。它的设计目标是易用性、gas 高效性、抗审查性和零手续费抽成。它对交易者非常有用,并且作为其他需要保证链上流动性的智能合约的组成部分,功能特别好。
大多数交易所都会维护一个订单簿,并促进买家和卖家之间的匹配。 Uniswap 智能合约持有各种代币的流动性储备,交易直接针对这些储备执行。价格是使用恒定乘积 (x*y=k) 做市商机制自动设定的,这使总体储备保持相对平衡。储备金集中在流动性提供者网络之间,流动性提供者向系统提供代币,以换取一定比例的交易费用。
Uniswap 的一个重要特征是利用工厂/注册合约,为每个 ERC20 代币部署单独的交换合约。这些交易合约同时持有 ETH 和关联的 ERC20 代币构成的准备金。这可以实现两个基于相关供应的交易对之间的交易。交易合约通过注册表串联在一起,从而可以使用 ETH 作为媒介,实现 ERC20 代币之间的互相交易。
本文档概述了 Uniswap 的核心机制和技术细节。为了可读性,一些代码被简化。省略了溢出检查和最低购买量等安全功能。完整的源代码可在 GitHub 上获取。
协议网站:
uniswap.io
形式化模型:
https://github.com/runtimeverification/verified-smart-contracts/blob/uniswap/uniswap/x-y-k.pdf)
Gas 基准测试
Uniswap 由于其简约的设计而非常高效。对于 ETH 到 ERC20 的交易,它使用的 Gas 几乎比 Bancor 少 10 倍。
它可以比 0x 更高效地执行 ERC20 到 ERC20 的交易,并且与 EtherDelta 和 IDEX 等链上订单簿交易所相比,可以显著减少 Gas 费用。
| Exchange | Uniswap | EtherDelta | Bancor | Radar Relay (0x) | IDEX | Airswap |
|---|---|---|---|---|---|---|
| ETH to ERC20 | 46,000 | 108,000 | 440,000 | 113,000* | 143,000 | 90,000 |
| ERC20 to ETH | 60,000 | 93,000 | 403,000 | 113,000* | 143,000 | 120,000* |
| ERC20 to ERC20 | 88,000 | no | 538,000 | 113,000 | no | no |
| *wrapped ETH |
在 Uniswap 上,直接 ERC20 代币转账成本为 36,000 Gas - 比 ETH 到 ERC20 交易便宜约 20%。
创建交易合约
uniswap_factory.vy 是一个智能合约,既充当 Uniswap 交易所的工厂又充当注册表。公共函数 createExchange() 允许以太坊用户为任何还没有交换合约的 ERC20 部署交换合约。
exchangeTemplate: public(address) token_to_exchange: address[address] exchange_to_token: address[address] @public def __init__(template: address): self.exchangeTemplate = template @public def createExchange(token: address) -> address: assert self.token_to_exchange[token] == ZERO_ADDRESS new_exchange: address = create_with_code_of(self.exchangeTemplate) self.token_to_exchange[token] = new_exchange self.exchange_to_token[new_exchange] = token return new_exchange
所有代币及其相关交换合约的记录都存储在工厂中。对于所有代币或交换合约地址,函数 getExchange() 和 getToken() 可用于查找另一个。
@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]
除了强制执行每个代币一次交易的限制之外,工厂合约在启动交换合约时不会对代币进行任何检查。用户和前端应该只与他们信任的代币交易所进行交互。
ETH ⇄ ERC20 Trades
Each exchange contract (uniswap_exchange.vy) is associated with a single ERC20 token and holds a liquidity pool of both ETH and that token. The exchange rate between ETH and an ERC20 is based on the relative sizes of their liquidity pools within the contract. This is done by maintaining the relationship eth_pool * token_pool = invariant. This invariant is held constant during trades and only changes when liquidity is added or removed from the market.
A simplified version of ethToTokenSwap(), the function for converting ETH to ERC20 tokens, is shown below:
eth_pool: uint256 token_pool: uint256 token: address(ERC20) @public @payable def ethToTokenSwap(): fee: uint256 = msg.value / 500 invariant: uint256 = self.eth_pool * self.token_pool new_eth_pool: uint256 = self.eth_pool + msg.value new_token_pool: uint256 = invariant / (new_eth_pool - fee) tokens_out: uint256 = self.token_pool - new_token_pool self.eth_pool = new_eth_pool self.token_pool = new_token_pool self.token.transfer(msg.sender, tokens_out)
Note: For gas efficiency eth_pool and token_pool are not stored variables. They are found using self.balance and through an external call to self.token.balanceOf(self)
When ETH is sent to the function eth_pool increases. In order to maintain the relationship eth_pool * token_pool = invariant, token pool is decreased by a proporitonal amount. The amount by which token_pool is decreased is the amount of tokens purchased. This change in reserve ratio shifts the ETH to ERC20 exchange rate, incentivizing trades in the opposite direction.
Exchanging tokens for ETH is done with the function tokenToEthSwap():
@public def tokenToEthSwap(tokens_in: uint256): fee: uint256 = tokens_in / 500 invariant: uint256 = self.eth_pool * self.token_pool new_token_pool: uint256 = self.token_pool + tokens_in new_eth_pool: uint256 = self.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 self.token.transferFrom(msg.sender, self, tokens_out) send(msg.sender, eth_out)
This increases token_pool and decreases eth_pool, shifting the price in the opposite direction. An example ETH to OMG purchase is shown below.
Example: ETH → OMG
Note: This example uses a fee of 0.25%. The real Uniswap contract has a fee of 0.3%.
10 ETH and 500 OMG (ERC20) are deposited into a smart contract by liquidity providers. An invariant is automatically set such that ETH_pool * OMG_pool = invariant.
ETH_pool = 10
OMG_pool = 500
invariant = 10 * 500 = 5000
An OMG buyer sends 1 ETH to the contract. A 0.25% fee is taken out for the liquidity providers, and the remaining 0.9975 ETH is added to ETH_pool. Next, the invariant is divided by the new amount of ETH in the liquidity pool to determine the new size of OMG_pool. The remaining OMG is sent to the buyer.
Buyer sends: 1 ETH
Fee = 1 ETH / 500 = 0.0025 ETH
ETH_pool = 10 + 1 - 0.0025 = 10.9975
OMG_pool = 5000/10.9975 = 454.65
Buyer receieves: 500 - 454.65 = 45.35 OMG
The fee is now added back into the liquidity pool, which acts as a payout to liquidity providers that is collected when liquidity is removed from the market. Since the fee is added after price calculation, the invariant increases slightly with every trade, making the system profitable for liquidity providers. In fact, what the invariant really represents is ETH_pool * OMG_pool at the end of the previous trade.
ETH_pool = 10.9975 + 0.0025 = 11
OMG_pool = 454.65
new invariant = 11 * 454.65 = 5,001.15
In this case the buyer received a rate of 45.35 OMG/ETH. However the price has shifted. If another buyer makes a trade in the same direction, they will get a slightly worse rate of OMG/ETH. However, if a buyer makes a trade in the opposite direction they will get a slightly better ETH/OMG rate.
1 ETH in
44.5 OMG out
Rate = 45.35 OMG/ETH
Purchases that are large relative to the total size of the liquidity pools will cause price slippage. In an active market, aribitrage will ensure that the price will not shift too far from that of other exchanges.

ERC20 ⇄ ERC20 Trades
Since ETH is used as a common pair for all ERC20 tokens, it can be used as an intermediary for direct ERC20 to ERC20 swaps. For example, it is possible to convert from OMG to ETH on one exchange and then from ETH to KNC on another within a single transaction.
To convert from OMG to KNC (for example), a buyer calls the function tokenToTokenSwap() on the OMG exchange contract:
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)
where token_addr is the address of KNC token and tokens_sold is the amount of OMG being sold. This function first checks the factory to retreive the KNC exchange address. Next, the exchange converts the input OMG to ETH. However instead of returning the purchased ETH to the buyer, the function instead calls the payable function ethToTokenTransfer() on the KNC exchange:
@public @payable def ethToTokenTransfer(recipent: address): fee: uint256 = msg.value / 500 invariant: uint256 = self.eth_pool * self.token_pool new_eth_pool: uint256 = self.eth_pool + msg.value new_token_pool: uint256 = invariant / (new_eth_pool - fee) tokens_out: uint256 = self.token_pool - new_token_pool self.eth_pool = new_eth_pool self.token_pool = new_token_pool self.invariant = new_eth_pool * new_token_pool self.token.transfer(recipent, tokens_out)
ethToTokenTransfer() receives the ETH and buyer address, verifies that the call is made from an exchange in the registry, converts the ETH to KNC, and forwards the KNC to the original buyer. ethToTokenTransfer() functions indentically to ethToTokenSwap() but has the additional input parameter recipient: address. This is used to forward purchased tokens to the original buyer instead of msg.sender, which in this case would be the OMG exchange.

交换 vs 转账
ethToTokenSwap()、tokenToEthSwap() 和 tokenToTokenSwap() 函数将购买的代币返回到买家地址。
ethToTokenTransfer()、tokenToEthTransfer() 和 tokenToTokenTransfer() 函数允许买家进行交易,然后立即将购买的代币转移到接收地址。
提供流动性
添加流动性
添加流动性需要将等值的 ETH 和 ERC20 代币存入 ERC20 代币的关联交易合约中。
第一个加入池的流动性提供者通过存入他们认为等值的 ETH 和 ERC20 代币来设定初始汇率。如果这个比率关闭,套利交易者将以牺牲初始流动性提供者的利益为代价使价格达到平衡。
所有未来的流动性提供者都使用存款时的汇率存入 ETH 和 ERC20。如果汇率不好,就有一个有利可图的套利机会来纠正价格。
流动性代币
流动性代币的铸造是为了跟踪每个流动性提供者贡献的总储备的相对比例。它们具有高度可分割性,可以随时销毁,以将市场流动性的比例份额返还给提供者。
流动性提供者调用 addLiquidity() 函数存入储备金并铸造新的流动性代币:
@public
@payable
def addLiquidity():
token_amount: uint256 = msg.value * token_pool / eth_pool
liquidity_minted: uint256 = msg.value * total_liquidity / eth_pool
eth_added: uint256 = msg.value
shares_minted: uint256 = (eth_added * self.total_shares) / self.eth_pool
tokens_added: uint256 = (shares_minted * self.token_pool) / self.total_shares)
self.shares[msg.sender] = self.shares[msg.sender] + shares_minted
self.total_shares = self.total_shares + shares_minted
self.eth_pool = self.eth_pool + eth_added
self.token_pool = self.token_pool + tokens_added
self.token.transferFrom(msg.sender, self, tokens_added)
铸造的流动性代币数量由发送到该函数的 ETH 数量决定。可以使用以下公式计算:
将 ETH 存入储备金还需要存入等值的 ERC20 代币。这是用以下等式计算的:
移除流动性
流动性提供者可以随时销毁其流动性代币,以从池中提取其相应份额的 ETH 和 ERC20 代币。
ETH和ERC20代币按当前汇率(准备金率)提取,而不是按其原始投资的比例提取。这意味着市场波动和套利可能会损失一些价值。
交易期间收取的费用将添加到总流动性池中,而无需铸造新的流动性代币。因此,ethWithdrawn 和 tokensWithdrawn 包含自首次添加流动性以来收取的所有费用的一定比例份额。
流动性代币
Uniswap 流动性代币代表流动性提供者对 ETH-ERC20 货币对的贡献。它们本身就是 ERC20 代币,并包含 EIP-20 的完整实现。
这使得流动性提供者可以出售他们的流动性代币或在账户之间转移它们,而无需从池中移除流动性。流动性代币专属于单个的ETH⇄ERC20交易所。该项目没有一个统一的ERC20代币。
Fee Structure
- ETH to ERC20 trades
- 0.3% fee paid in ETH
- ERC20 to ETH trades
- 0.3% fee paid in ERC20 tokens
- ERC20 to ERC20 trades
- 0.3% fee paid in ERC20 tokens for ERC20 to ETH swap on input exchange
- 0.3% fee paid in ETH for ETH to ERC20 swap on output exchange
- Effectively 0.5991% fee on input ERC20
There is a 0.3% fee for swapping between ETH and ERC20 tokens. This fee is split by liquidity providers proportional to their contribution to liquidity reserves. Since ERC20 to ERC20 trades include both an ERC20 to ETH swap and an ETH to ERC20 swap, the fee is paid on both exchanges. There are no platform fees.
Swapping fees are immediately deposited into liquidity reserves. Since total reserves are increased without adding any additional share tokens, this increases that value of all share tokens equally. This functions as a payout to liquidity providers that can be collected by burning shares.
Since fees are added to liquidity pools, the invariant increases at the end of every trade. Within a single transaction, the invariant represents eth_pool * token_pool at the end of the previous transaction.
Custom Pools
ERC20 to Exchange
The additional functions tokenToExchangeSwap() and tokenToExchangeTransfer() add to Uniswap’s flexibility. These functions convert ERC20 tokens to ETH and attempts an ethToTokenTransfer() at a user input address. This allows ERC20 to ERC20 trades against custom Uniswap exchanges that do not come from the same factory, as long as they implement the proper interface. Custom exchanges can have different curves, managers, private liquidity pools, FOMO-based ponzi schemes, or anything else you can think of.
Opt-in Upgrades
Upgrading censorship resistant, decentralized smart contracts is hard. Hopefully Uniswap 1.0 is perfect but it probably is not. If an improved Uniswap 2.0 design is created, a new factory contract can be deployed. Liquidity providers can choose to move to the new system or stay in the old one.
The tokenToExchange functions enable trades with exchanges launched from different factories. This can be used for backwards compatibility. ERC20 to ERC20 trades will be possible within versions using both tokenToToken and tokenToExchange functions. However, across versions only tokenToExchange will work. All upgrades are opt-in and backwards compatible.
Frontrunning
Uniswap can be frontrun to some extent. This is bounded by user set minimum/maximum values and transaction deadlines.