UniswapV1-V2

发布于 2026年1月3日
178 分钟阅读

一. 重点内容

  • 去中心化交易所
    • Uniswap V1
    • Uniswap V2

二.去中心化交易所

Cex 都是基于 orderbook 模式来做

1. 基于订单薄的去中心化交易所

  • 在合约里面实现订单薄的模式,交易是通过买单和卖单匹配策略来确定的,买卖双方可以按照自己的想法设置买卖价格
    • Etherdelta: 所有都放到链上
    • 0x: 撮合系统放在链下,执行结果放到链上
    • 基于 dex 优缺点
      • 按照中心化交易所这套来实施,这方式效率极低,收费高
      • 低流动性:流动分散
      • 交易速度慢
      • 用户体验

2. 自动做市商模型

  • 自动做市商模式,不再基于订单薄模式,而是使用流动性池来做,核心公式
X * Y = K 
  • 流动性池操作模式

    • 添加流动性: 使用 X 去匹配 Y,或者使用 Y 匹配 X; ETH/USDT 2000;
    • 撤出流动性:减少 X 要减少对应的 Y 值, ETH/USDT 2000
  • 交易的过程(ETH/USDT)

    • 开始状态

      • ETH:100
      • USDT:40000
      • 开始状态 X * Y = K ===> 100 * 40000 = 4000000
    • 用户 A 用 10 ETH 去购买 USDT

      • 将 10 ETH 放到 ETH POOL,交易完成之后,ETH 数量会变成 110 ETH

      • 计算交易后USDT池应有的数量

        根据恒定乘积公式 X∗Y=K

        • 110 * Y = 4000000 ==> Y = 4000000 / 110 = 36363.64

        计算用户应获得的USDT数量:

        • 40,000−36,363.64=3,636.36

3. DEX 里面的其他的一些概念

  • 价格滑点:交易完成之后,ETH 池子数量增加,DAI 池子的数量减少,用户实际获得 3,636.36 USDT,相比初始状态价格10:4000 有滑点。
  • 无偿损失:要想减少无偿损失,需要去池深度好的里面去进行交易
  • LP:在 Uniswap 里面提供流动性叫流动性提供者,流动性提供者是可以获得手续费收益的,在 uniswap v3,可以获得 3% 的手续费收益
  • LP Token: 在 Uniswap 里面添加流动性得到 UNI 的 LP Token, 在 uniswap v3 和 v4 里面是 nft
  • 价格区间:在 Uniswap V2 里面提供流动性是广域的,在这个 (0, 正无穷大)里面提供流动性; 但是 uniswap v3, v4 是在价格区间内提供流动性
  • 自定义逻辑编程:uniswap v4 支持编程 hook

Uniswap v1-v4:

  • 2018 年 11 月 Uniswap V1 发布,创新性地采用了上述 CPMM,支持 ERC-20 和 ETH 的兑换,为后续版本的 Uniswap 奠定了基础,并成为其他 AMM 协议的启示;
  • 2020 年 5 月 Uniswap V2 发布, 在 V1 的基础上引入了 ERC-20 之间的兑换,以及时间加权平均价格(TWAP)预言机,增加交易对的灵活性,巩固了 Uniswap 在 DEX 的领先地位;
  • 2021 年 5 月 Uniswap V3 发布,引入了集中流动性(Concentrated Liquidity),允许 LP 在交易对中定义特定的价格范围,以实现更精确的价格控制,提升了 LP 的资产利用率;
  • 2023 年 6 月 Uniswap V4 公开了白皮书的草稿版本,引入了 Hook、Singleton、Flash Accounting 和原生 ETH 等多个优化,其中 Hook 是最重要的创新机制,给开发者提供了高度自定义性。
  • 2025年1月Uniswap V4 发布,V4 版本在算法上并没有改变,依然还是采用集中流动性,但通过 Hooks 实现了可定制的池,单例合约和闪电记账大幅度降低了 gas 成本,对原生 ETH 的支持也同样减少了 gas,还有对动态费用的支持、ERC1155 的支持等,都大大提高了 Uniswap 的灵活性、可扩展性。

4. 闪电贷

闪电贷是一种无需提供任何抵押品,但必须在同一笔区块链交易内“借入并归还”的贷款。如果无法归还,整个交易会被撤销,如同从未发生过。

4.1 核心原理:原子性与回滚

  1. 原子性: 在区块链中,一笔交易要么完全成功(所有操作都执行),要么完全失败(所有状态回滚,像没发生过一样)。没有中间状态。
  2. 无需抵押: 因为规则设定为“必须归还”,所以贷款方(资金池)没有资产损失的风险。它只是在交易执行的“一瞬间”把资金的使用权借给你。
  3. 套利引擎: 这给予了普通人(套利者、开发者)一种强大的金融工具:短暂地获得巨额资本,去捕捉微小的市场机会

4.2 工作流程(以套利为例)

假设你发现:

  • 在DEX A里,1 ETH = 1900 USDT
  • 在DEX B里,1 ETH = 1910 USDT
  • 你有10 USDT,但这点钱无法套利。

没有闪电贷: 你只能看着机会溜走。 使用闪电贷:

  1. 发起交易: 你编写一个智能合约,发起一笔包含多个操作的复杂交易。
  2. 借入巨款: 你的合约瞬间从闪电贷资金池借出1000个ETH(价值约190万美元)。
  3. 执行策略
    • 在DEX A,用1000 ETH买入USDT:得到
      1000 * 1900 = 1,900,000 USDT
    • 在DEX B,用这1,900,000 USDT买入ETH:得到
      1,900,000 / 1910 ≈ 994.764 ETH
  4. 归还贷款 + 支付费用
    • 归还: 你必须归还借入的1000 ETH。
    • 利息: 同时支付一小笔手续费(例如0.09%),假设是0.9 ETH。
    • 计算:你手头有994.764 ETH,但需要还1000.9 ETH?显然不够!交易会失败回滚。
  5. 关键点——套利成功的情况
    • 如果价差足够大,比如在DEX B卖出后你得到了 1001.5个ETH
    • 那么流程变为:
      1001.5 ETH - 1000 ETH(本金)- 0.9 ETH(利息)= 0.6 ETH
    • 这0.6 ETH就是你的无风险利润。交易成功,利润留在你的合约地址中。

整个过程在区块链上一个区块时间内(如以太坊约12秒)完成,要么全部成功,要么全部无效。

图片描述图片描述

三.Uniswap V1 的代码讲解

Vyper 预言,类似 Python, 也是以太坊的智能合约编程语言

1. Vyper 语法简介

Vyper 是一种智能合约编程语言,专为以太坊虚拟机(EVM)设计,注重安全性、简洁性和可读性。Vyper 的设计目标是成为一种更简单、更加安全的语言,减少常见的安全漏洞,并限制复杂的功能。相比 Solidity,Vyper 语法更为简洁,易于理解,并且摒弃了一些可能导致代码复杂性和错误的特性。

1.1 Vyper 的特点

  • 简洁的语法:Vyper 通过限制语言功能来保持代码简洁易读。
  • 安全性:去掉了一些容易出现安全漏洞的特性,如循环、递归和函数重载等。
  • 审计友好:Vyper 的目标之一是使智能合约更容易被人类审计。

1.2 Vyper 的基本语法

Vyper 的语法风格类似于 Python,它使用缩进来表示代码块的层次结构。

1.2.1 变量声明

在 Vyper 中,变量需要声明其类型。这与 Solidity 的隐式类型声明不同,Vyper 强制变量有明确的类型。

# 声明一个整数变量
count: public(int128)

# 声明一个地址变量
owner: public(address)

# 声明一个 map(类似于 Solidity 中的 mapping)
balances: public(map(address, uint256))

1.2.2 函数定义

Vyper 使用

@external
@internal
装饰器来标识外部或内部函数。外部函数可以被合约外部调用,而内部函数只能在合约内部调用。

@external
def set_count(_count: int128):
    self.count = _count

@internal
def _add(x: int128, y: int128) -> int128:
    return x + y
  • @external
    :用于声明外部可调用的函数。
  • @internal
    :用于声明仅能在合约内部调用的函数。
  • -> type
    :表示返回值的类型。

1.2.3 常量

Vyper 支持常量的声明,常量值一旦定义便不可更改。

MAX_SUPPLY: constant(uint256) = 1000000

1.2.4 状态变量的访问权限

Vyper 中可以通过

public
修饰符使状态变量公开访问,这样会自动生成对应的 getter 函数。

balance: public(uint256)

上述声明会自动生成

balance()
函数,使得外部可以查询该变量的值。

1.2.5 事件(Events)

Vyper 支持事件,用于在链上发出通知。

event Transfer:
    _from: indexed(address)
    _to: indexed(address)
    _value: uint256
  • indexed
    关键词用于将事件参数索引化,使得它们可以在日志中被更有效地搜索到。

1.2.6 条件控制

Vyper 支持

if-else
条件控制语句。

@external
def transfer(_to: address, _amount: uint256):
    if self.balances[msg.sender] >= _amount:
        self.balances[msg.sender] -= _amount
        self.balances[_to] += _amount
    else:
        raise "Insufficient balance"
  • if
    后跟条件,缩进的代码块是条件为
    True
    时执行的代码。
  • raise
    用于抛出错误信息,当条件不满足时,交易会失败。

1.2.7 结构体(Structs)

Vyper 支持结构体,类似于 Solidity 中的

struct
,用于将多个数据聚合成一个类型。

struct Person:
    name: String[64]
    age: int128

person: public(Person)

在上述例子中,

Person
是一个结构体,包含了
name
age
两个字段。

1.2.8 消息(msg)和交易(tx)对象

Vyper 中的

msg
tx
对象类似于 Solidity。

# 访问发送者地址
sender: address = msg.sender

# 访问交易发起者的以太坊余额
balance: uint256 = self.balance

1.2.9 函数修饰符

在 Vyper 中,函数修饰符(Function Modifiers)与 Solidity 的类似,但由于 Vyper 的设计理念侧重简洁和安全性,它并没有 Solidity 中专门的“修饰符”功能(例如

modifier
关键字)。不过,Vyper 仍然通过函数的装饰器(Decorator)实现了某些类似修饰符的功能,比如控制函数的访问权限、允许支付以太币等。以下是 Vyper 中常用的函数修饰符(装饰器):

1.2.9.1 @external

@external
修饰符用于定义外部可调用的函数。这意味着合约外部的用户或其他智能合约可以调用带有
@external
装饰器的函数。

@external
def set_value(_value: int128):
    self.value = _value
  • 这是外部可调用函数的标志,相当于 Solidity 中的
    external
1.2.9.2 @internal

@internal
修饰符用于定义只能由合约内部调用的函数。这意味着只有该合约中的其他函数才能调用这个函数,而外部调用将无法访问它。

@internal
def _add(x: int128, y: int128) -> int128:
    return x + y
  • @internal
    修饰符使得函数的访问权限仅限于合约内部,相当于 Solidity 中的
    internal
1.2.9.3 @payable

@payable
修饰符允许一个函数接收以太币。带有
@payable
修饰符的函数可以接受发送到合约的以太币。

@external
@payable
def deposit():
    self.balances[msg.sender] += msg.value
  • @payable
    相当于 Solidity 中的
    payable
    ,表示该函数可以接收以太币。
1.2.9.4 @view

@view
修饰符用于声明该函数不会修改链上的状态(不会改变合约的存储)。带有
@view
修饰符的函数仅用于读取链上数据,不会消耗 gas。

@external
@view
def get_balance(_addr: address) -> uint256:
    return self.balances[_addr]
  • @view
    修饰符相当于 Solidity 中的
    view
    ,表示函数仅读取数据而不修改状态。
1.2.9.5 @pure

@pure
修饰符用于声明该函数既不会读取合约的存储,也不会修改链上的状态。它只能使用函数输入参数进行计算或操作。

@external
@pure
def multiply(a: int128, b: int128) -> int128:
    return a * b
  • @pure
    修饰符类似于 Solidity 中的
    pure
    ,表示函数既不读取合约存储,也不修改状态。
1.2.9.6 @nonpayable

Vyper 中并没有直接的

@nonpayable
修饰符,但默认情况下,任何没有
@payable
修饰符的函数都会被视为非支付函数。如果你不希望函数接受以太币,只需不加
@payable
即可。

@external
def set_owner(_owner: address):
    self.owner = _owner
1.2.9.7 自定义逻辑的修饰符

Vyper 不支持像 Solidity 那样的自定义修饰符(

modifier
关键字),不过你可以在每个函数内部实现自定义的权限控制或其他逻辑。例如,实现一个只有合约拥有者才能调用的函数:

owner: public(address)

@external
def __init__():
    self.owner = msg.sender

@internal
def _only_owner():
    assert msg.sender == self.owner, "Not authorized"

@external
def set_new_owner(_new_owner: address):
    self._only_owner()
    self.owner = _new_owner

在这个例子中,

_only_owner()
函数充当了自定义的权限检查修饰符。

1.3 与 Solidity 的区别

  • 不支持循环:Vyper 不支持
    for
    while
    循环,旨在减少复杂性,并防止 gas 消耗过多的情况。
  • 不支持递归:递归的使用可能会导致堆栈深度超过限制,因此 Vyper 禁止递归调用。
  • 没有函数重载:Vyper 不支持函数重载,每个函数名在合约中必须唯一。
  • 无继承:为了保持简单性,Vyper 不支持继承,这减少了合约之间的依赖性。
  • 可读性优先:Vyper 通过减少特性来优先提高代码的可读性,使得合约更容易被审计。

1.3.1 例合约:简单的代币合约

以下是一个基于 Vyper 的简单 ERC20 代币合约示例:

# ERC20 代币标准

name: public(String[64])
symbol: public(String[32])
decimals: public(uint256)
total_supply: public(uint256)

balances: public(map(address, uint256))
allowances: public(map(address, map(address, uint256)))

event Transfer:
    sender: indexed(address)
    receiver: indexed(address)
    amount: uint256

event Approval:
    owner: indexed(address)
    spender: indexed(address)
    amount: uint256

@external
def __init__(_name: String[64], _symbol: String[32], _decimals: uint256, _initial_supply: uint256):
    self.name = _name
    self.symbol = _symbol
    self.decimals = _decimals
    self.total_supply = _initial_supply
    self.balances[msg.sender] = _initial_supply

@external
def transfer(_to: address, _amount: uint256) -> bool:
    assert self.balances[msg.sender] >= _amount, "Insufficient balance"
    self.balances[msg.sender] -= _amount
    self.balances[_to] += _amount
    log Transfer(msg.sender, _to, _amount)
    return True

@external
def approve(_spender: address, _amount: uint256) -> bool:
    self.allowances[msg.sender][_spender] = _amount
    log Approval(msg.sender, _spender, _amount)
    return True

@external
def transferFrom(_from: address, _to: address, _amount: uint256) -> bool:
    assert self.allowances[_from][msg.sender] >= _amount, "Allowance exceeded"
    assert self.balances[_from] >= _amount, "Insufficient balance"
    self.allowances[_from][msg.sender] -= _amount
    self.balances[_from] -= _amount
    self.balances[_to] += _amount
    log Transfer(_from, _to, _amount)
    return True

1.4 总结

Vyper 提供了一种简单、安全的智能合约编写方式,其限制有助于减少潜在的安全漏洞并提高代码的可读性。尽管功能比 Solidity 更有限,但对于特定的应用场景,尤其是需要高度安全性的应用场景,Vyper 是一种非常有吸引力的选择。

2. UniSwap V1 源码解读

创建交易合约模板地址

uniswapV1_创建模版合约uniswapV1_创建模版合约

exchangeTemplate: public(address)

@public
def initializeFactory(template: address):
    // 入参 模板合约地址
    assert self.exchangeTemplate == ZERO_ADDRESS
    assert template != ZERO_ADDRESS
    // 设置模板合约地址
    self.exchangeTemplate = template

创建新的交易所合约

uniswapV1_创建新的交易所合约uniswapV1_创建新的交易所合约

@public
def createExchange(token: address) -> address:
    // 校验入参不是零地址
    assert token != ZERO_ADDRESS
    // 校验模板合约地址不是零地址
    assert self.exchangeTemplate != ZERO_ADDRESS
    // 入参地址,在历史处理中,不存在
    assert self.token_to_exchange[token] == ZERO_ADDRESS
    // 根据模板合约地址,创建exchange
    exchange: address = create_with_code_of(self.exchangeTemplate)
    // 使用入参token,初始化Exchange
    Exchange(exchange).setup(token)
    // 在多个mapping中设置相关信息
    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
    
// 在mapping中,可以通过token地址,获取到exchange地址
// 

添加流动性

uniswapV1_添加流动性uniswapV1_添加流动性

// 在Vyper中,@payable 修饰符允许函数接收ETH。当用户调用带有 @payable 修饰符的函数时,
// ETH会自动从用户的地址转移到当前合约中。因此,在 addLiquidity 函数中,
// ETH是在用户调用该函数时自动转移到合约中的。

min_liquidity:用户期望的最小流动性代币数量。
max_tokens:用户愿意提供的最大代币数量。
deadline:交易的最后执行时间。
返回值:铸造的流动性代币数量。

@public
@payable
def addLiquidity(min_liquidity: uint256, max_tokens: uint256, deadline: timestamp) -> uint256:
    // 确保操作在结束时间前执行,入参的token都是正常的数据
    // 确保用户提供的最大代币数量和ETH数量大于0
    assert deadline > block.timestamp and (max_tokens > 0 and msg.value > 0)
    // 获取当前交易对的UNI代币总供应量
    total_liquidity: uint256 = self.totalSupply
    if total_liquidity > 0:
        // 处理已有流动性池的情况
        
        // 确保入参的最小流动性大于0
        assert min_liquidity > 0
        // 计算合约中的eth储备,需要减去用户入参的eth
        eth_reserve: uint256(wei) = self.balance - msg.value
        // 获取合约中当前的代币储备量
        token_reserve: uint256 = self.token.balanceOf(self)
        // 计算需要的代币数量,公式为 msg.value * token_reserve / eth_reserve + 1
        // 确保按当前ETH和代币的比例添加流动性。
        token_amount: uint256 = msg.value * token_reserve / eth_reserve + 1
        // 计算铸造的流动性代币数量,公式为 msg.value * total_liquidity / eth_reserve
        liquidity_minted: uint256 = msg.value * total_liquidity / eth_reserve
        // 确保最大代币数量和铸造的流动性数量满足条件
        assert max_tokens >= token_amount and liquidity_minted >= min_liquidity
        // 增加用户的UNI代币余额
        self.balances[msg.sender] += liquidity_minted
        // 设置合约的UNI代币总发行量
        self.totalSupply = total_liquidity + liquidity_minted
        // 从用户地址,转移token到当前合约中
        assert self.token.transferFrom(msg.sender, self, token_amount)
        // 记录添加流动性事件,用户转移了eth,token到当前合约中
        log.AddLiquidity(msg.sender, msg.value, token_amount)
        // 记录转账事件,从合约地址,转移了代币到用户地址中
        log.Transfer(ZERO_ADDRESS, msg.sender, liquidity_minted)
        // 返回初始流动性代币数量
        return liquidity_minted
    else:
        // 如果是当前交易对的第一笔交易
        
        assert (self.factory != ZERO_ADDRESS and self.token != ZERO_ADDRESS) and msg.value >= 1000000000
        // 确保工厂获取的交易所地址为当前合约
        assert self.factory.getExchange(self.token) == self
        // 定义一个参数,等于入参 max_tokens
        token_amount: uint256 = max_tokens
        // 初始化流动性 = 当前合约的eth数量
        initial_liquidity: uint256 = as_unitless_number(self.balance)
        // 当前交易对的uni代币的总供应量 = 初始化流动性
        self.totalSupply = initial_liquidity
        // 设置用户的UNI代币余额
        // 获得的UNI代币数量等于合约中持有的ETH数量
        self.balances[msg.sender] = initial_liquidity
        // 从用户地址,转移token到当前合约中
        assert self.token.transferFrom(msg.sender, self, token_amount)
        // 记录添加流动性事件,用户转移了eth,token到当前合约中
        log.AddLiquidity(msg.sender, msg.value, token_amount)
        // 记录转账事件,从合约地址,转移了代币到用户地址中
        log.Transfer(ZERO_ADDRESS, msg.sender, initial_liquidity)
        // 返回初始流动性代币数量
        return initial_liquidity

删除流动性

uniswapV1_删除流动性uniswapV1_删除流动性

amount:用户希望移除的流动性代币(UNI)的数量。
min_eth:用户希望提取的最小ETH数量。
min_tokens:用户希望提取的最小代币数量。
deadline:交易的最后执行时间。

允许用户从流动性池中移除流动性,并按比例提取ETH和代币。通过一系列的验证和计算,确保用户能够获得预期的ETH和代币数量,
并且交易在指定的时间内完成

@public
def removeLiquidity(amount: uint256, min_eth: uint256(wei), min_tokens: uint256, deadline: timestamp) -> (uint256(wei), uint256):
    // 确保amount、min_eth、min_tokens大于0,并且当前时间小于deadline。
    assert (amount > 0 and deadline > block.timestamp) and (min_eth > 0 and min_tokens > 0)
    // 获取当前合约中总的UNI代币供应量
    total_liquidity: uint256 = self.totalSupply
    // 确保总的UNI供应量大于0
    assert total_liquidity > 0
    // 获取当前合约中持有的代币数量
    token_reserve: uint256 = self.token.balanceOf(self)
    // 根据用户移除的流动性代币数量,按比例计算提取的ETH数量
    eth_amount: uint256(wei) = amount * self.balance / total_liquidity
    // 根据用户移除的流动性代币数量,按比例计算提取的代币数量
    token_amount: uint256 = amount * token_reserve / total_liquidity
    // 确保提取的ETH和代币数量不低于用户期望的最小值
    assert eth_amount >= min_eth and token_amount >= min_tokens
    // 减少用户的UNI代币余额
    self.balances[msg.sender] -= amount
    // 更新总的UNI代币供应量
    self.totalSupply = total_liquidity - amount
    // 发送计算出的ETH数量给用户
    send(msg.sender, eth_amount)
    // 转移计算出的代币数量给用户
    assert self.token.transfer(msg.sender, token_amount)
    // 记录移除流动性事件
    log.RemoveLiquidity(msg.sender, eth_amount, token_amount)
    // 记录转账事件
    log.Transfer(msg.sender, ZERO_ADDRESS, amount)
    return eth_amount, token_amount

getInputPrice

根据恒定乘积进行计算

input_amount:用户提供的输入数量(代币或ETH)。
input_reserve:交易所中输入类型的储备数量(代币或ETH)。
output_reserve:交易所中输出类型的储备数量(代币或ETH)。

使用恒定乘积公式来计算在给定输入数量的情况下,可以获得的输出数量。
它考虑了交易手续费,并确保输入和输出储备大于0。
这个方法在代币交换过程中用于确定用户提供的输入(ETH或代币)可以换取多少输出(代币或ETH)。
@private
@constant
def getInputPrice(input_amount: uint256, input_reserve: uint256, output_reserve: uint256) -> uint256:
    // 确保输入和输出储备大于0
    assert input_reserve > 0 and output_reserve > 0
    // 计算包含手续费的输入数量。这里的997是为了考虑交易手续费(0.3%)
    input_amount_with_fee: uint256 = input_amount * 997
    // 计算分子 分子计算公式
    numerator: uint256 = input_amount_with_fee * output_reserve
    // 计算分母 这里的1000是为了放大数值,避免小数计算
    denominator: uint256 = (input_reserve * 1000) + input_amount_with_fee
    // 返回可以获得的输出数量
    return numerator / denominator

getOutputPrice

根据恒定乘积进行计算

output_amount:用户希望获得的输出数量(代币或ETH)。
input_reserve:交易所中输入类型的储备数量(代币或ETH)。
output_reserve:交易所中输出类型的储备数量(代币或ETH)。

// 使用恒定乘积公式来计算在给定输出数量的情况下,需要的输入数量。
// 它考虑了交易手续费,并确保输入和输出储备大于0。
// 在代币交换过程中用于确定用户需要提供多少输入(ETH或代币)才能获得指定数量的输出(代币或ETH)。
@private
@constant
def getOutputPrice(output_amount: uint256, input_reserve: uint256, output_reserve: uint256) -> uint256:
    // 确保输入和输出储备大于0
    assert input_reserve > 0 and output_reserve > 0
    // 计算分子
    // 这里的1000是为了放大数值,避免小数计算
    numerator: uint256 = input_reserve * output_amount * 1000
    // 计算分母
    // 这里的997是为了考虑交易手续费(0.3%)
    denominator: uint256 = (output_reserve - output_amount) * 997
    // 返回需要的输入数量
    // +1是为了向上取整,确保输入数量足够覆盖输出数量
    return numerator / denominator + 1

交易-ETH到代币

uniswapV1_交易-ETH到代币uniswapV1_交易-ETH到代币

eth_sold:用户卖出的ETH数量。
min_tokens:用户希望买入的最小代币数量。
deadline:交易的最后执行时间。
buyer:买入代币的用户地址。
recipient:接收代币的地址。
返回值:买入的代币数量。

将用户卖出的ETH转换为代币,并将代币转移给指定的接收者。
通过一系列的验证和计算,确保用户能够获得预期的代币数量,并且交易在指定的时间内完成。

@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)
    // 根据恒定乘积公式,卖出的ETH,ETH储备,代币储备,计算买入代币数量
    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)
    // 记录代币购买事件,包含买家地址、卖出的ETH数量和买入的代币数量
    log.TokenPurchase(buyer, eth_sold, tokens_bought)
    // 返回买入的代币数量
    return tokens_bought

交易-代币到ETH

uniswapV1_交易-代币到ETHuniswapV1_交易-代币到ETH

tokens_sold:用户卖出的代币数量。
min_eth:用户希望买入的最小ETH数量。
deadline:交易的最后执行时间。
buyer:卖出代币的用户地址。
recipient:接收ETH的地址。
返回值:买入的ETH数量(以wei为单位)。

将用户卖出的代币转换为ETH,并将ETH转移给指定的接收者。
通过一系列的验证和计算,确保用户能够获得预期的ETH数量,并且交易在指定的时间内完成

@private
def tokenToEthInput(tokens_sold: uint256, min_eth: uint256(wei), deadline: timestamp, buyer: address, recipient: address) -> uint256(wei):
    // 确保tokens_sold和min_eth大于0,并且当前时间小于等于deadline。
    assert deadline >= block.timestamp and (tokens_sold > 0 and min_eth > 0)
    // 获取当前合约中持有的代币数量
    token_reserve: uint256 = self.token.balanceOf(self)
    // 根据恒定乘积公式,卖出的代币数量,代币储备,ETH储备,计算买入ETH数量
    eth_bought: uint256 = self.getInputPrice(tokens_sold, token_reserve, as_unitless_number(self.balance))
    // 将买入的ETH数量转换为wei单位
    wei_bought: uint256(wei) = as_wei_value(eth_bought, 'wei')
    // 确保买入的ETH数量不低于用户期望的最小值
    assert wei_bought >= min_eth
    // 将计算出的ETH数量发送给接收者
    send(recipient, wei_bought)
    // 从买家地址转移卖出的代币数量到合约
    assert self.token.transferFrom(buyer, self, tokens_sold)
    // 记录ETH购买事件,包含买家地址、卖出的代币数量和买入的ETH数量
    log.EthPurchase(buyer, tokens_sold, wei_bought)
    return wei_bought

交易-代币1到代币2

uniswapV1_交易-代币到代币uniswapV1_交易-代币到代币

tokens_sold:用户卖出的代币数量。
min_tokens_bought:用户希望买入的最小目标代币数量。
min_eth_bought:用户希望买入的最小ETH数量。
deadline:交易的最后执行时间。
buyer:卖出代币的用户地址。
recipient:接收目标代币的地址。
exchange_addr:目标交易所的地址。
返回值:买入的目标代币数量。

将用户卖出的代币转换为ETH,然后再将ETH转换为另一种代币,并将最终的代币转移给指定的接收者。
通过一系列的验证和计算,确保用户能够获得预期的目标代币数量,并且交易在指定的时间内完成

@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:
    // 确保tokens_sold、min_tokens_bought和min_eth_bought大于0,并且当前时间小于等于deadline。
    assert (deadline >= block.timestamp and tokens_sold > 0) and (min_tokens_bought > 0 and min_eth_bought > 0)
    // 确保 exchange_addr 有效且不等于当前合约地址
    assert exchange_addr != self and exchange_addr != ZERO_ADDRESS
    // 获取当前交易对中持有的代币数量
    token_reserve: uint256 = self.token.balanceOf(self)
    // 根据恒定乘积公式,卖出的代币数量、代币储备和ETH储备计算买入的ETH数量
    eth_bought: uint256 = self.getInputPrice(tokens_sold, token_reserve, as_unitless_number(self.balance))
    // 将计算出的ETH数量转换为wei单位
    wei_bought: uint256(wei) = as_wei_value(eth_bought, 'wei')
    // 确保买入的ETH数量不低于用户期望的最小值
    assert wei_bought >= min_eth_bought
    // 从买家地址转移卖出的代币数量到合约
    assert self.token.transferFrom(buyer, self, tokens_sold)
    // 调用目标交易所的ethToTokenTransferInput函数,将ETH转换为目标代币并转移给接收者
    tokens_bought: uint256 = Exchange(exchange_addr).ethToTokenTransferInput(min_tokens_bought, deadline, recipient, value=wei_bought)
    // 记录ETH购买事件,包含买家地址、卖出的代币数量和买入的ETH数量
    log.EthPurchase(buyer, tokens_sold, wei_bought)
    return tokens_bought

3. 总结

下图展示了Uniswap V1 智能合约的基本架构和交易流程:

uniswapV1_智能合约的基本架构和交易流程uniswapV1_智能合约的基本架构和交易流程

uniswap_factory.vy:这是一个工厂合约,负责创建新的交易合约(exchange)。每个交易合约对应一个特定的代币对。

uniswap_exchange.vy:这是交易合约的核心,处理所有交易相关的逻辑,主要功能分为:

  • 添加流动性(流动池充值)
  • 删除流动性(流动池提取)
  • 交换功能,包括:
    • ETH兑Token:用户可以将以太坊(ETH)兑换成其他代币。
    • Token兑ETH:用户可以将持有的代币兑换成以太坊。
    • Token兑Token(通过以太坊作为中介):用户可以将一种代币直接兑换成另一种代币,通过中间的以太坊转换。

四.Uniswap V2 的代码解读

1. Uniswap V2 相对于 V1 的变化

Uniswap V2 是 Uniswap 去中心化交易所的第二个版本,发布于 2020 年 5 月。与其前身 Uniswap V1 相比,V2 引入了一些显著的改进和新功能,提升了流动性提供、交易效率以及用户体验。

1.1 合约代码用 solidity 重构

  • Uniswap V1 合约是用 Vyper 写的,Uniswap V2 用 solidity 重写了

1.2 ERC20 到 ERC20 交易对

  • 在 Uniswap V1 中,每个交易对都必须包含以太坊(ETH)作为中介,意味着 ERC20 代币之间的交易需要通过 ETH 进行两步交换:从 ERC20 转换为 ETH,再从 ETH 转换为另一种 ERC20 代币。
  • Uniswap V2 支持 ERC20 到 ERC20 直接交易,这不仅简化了交易流程,还减少了交易手续费和滑点。

1.3 闪电交换(Flash Swaps)

  • 闪电交换: 允许用户无需预先支付代币即可借出合约中的任意数量的资产,只要在交易结束前支付相应的代币,或在交易过程中销毁代币。
  • 这种机制类似于 Aave 或 dYdX 的闪电贷,用户可以在单个原子交易中借入和偿还资产,极大地提升了去中心化金融(DeFi)的创新能力。

1.4 TWAP

  • Uniswap V2 中引入了内置的时间加权平均价格(TWAP)预言机功能。这使得开发者可以通过调用 Uniswap 合约获取价格信息,用于其他去中心化应用(Dapps)中,如借贷协议、衍生品等。
  • TWAP 价格计算基于一定时间段内的价格均值,能够抵抗短期波动和攻击,增强了系统的稳健性。

1.5 增加了统一入口 Router 合约

  • Uniswap V2 的入口在 Router 合约

1.6 可定制的智能合约架构

  • Uniswap V2 的合约架构更加模块化和灵活,支持自定义 LP 代币、手续费模型等功能。这使得开发者可以基于 Uniswap V2 创建更加复杂的金融工具和服务。

1.7 安全性增强

  • Uniswap V2 通过升级合约架构来增强安全性,包括更强的输入验证、更好的防止重入攻击机制等,减少了潜在的智能合约漏洞。

2. Uniswap V2 的代码架构

uniswapV2-代码架构uniswapV2-代码架构

3. UniSwap V2 的原理

AMM: X * Y = L^2 (k)

K 越大,兑换交易的滑点越低

LP: 为池子提供流动性,可以获得对应的 LP Token, 早期的 LP 是可以质押到 Uniswap 获取,质押合约目前已经废弃了

Lp Fee = 你的流动性 / 总流动性 * 0.3%

4. Uniswap V2 的合约结构

  • Factory 合约: 负责创建或者管理交易对的 (在V2-core)
  • Pair 合约:每个交易对创建一个 pair 合约, 负责管理特定 token 对的流动性,交易 (在V2-core)
  • Router 合约: 负责管理用户交互,帮忙用户完成添加流动性,移除流动和 swap 等过的合约 (在V2-periphery)

uniswapV2_合约结构图uniswapV2_合约结构图

5. UniSwap V2 的代码实现细节

5.0 添加池子(createPair)

通过UniswapV2Router02 调用_addLiquidity

IUniswapV2Factory(factory).createPair(tokenA, tokenB) ->

[v2-core] UniswapV2Factory.sol中的 createPair

添加pair添加pair

5.1 添加流动

5.1.1 添加流动性算法

(1) pool的shares和流动性的关系

核心思想就是等比例增加/减少:池子里value增加/减少的比例=shares增加/减少的比例

mint Shares (用户存钱,该返回多少shares)

mintShares公式mintShares公式

Example:

mintShares例子mintShares例子

burn shares (用户销毁shares,该退回多少USDC)

burnSharesburnShares

example:

burnSharesExampleburnSharesExample

(2) 添加流动性如何计算要添加多少token(dx,dy)

核心:要保持添加流动性后,这个池子的币对间的price不变

添加流动性算法添加流动性算法

(3) 结合以上公式计算要mint的poolShares

计算mintshares计算mintshares

5.1.2 添加流动性-流程图

添加流动性流程图添加流动性流程图

5.1.3 添加流动性-合约代码流程图

画板链接

5.2 移除流动

5.2.1 移除流动性算法:

(1) dx,dy和增加流动性一样
  1. dy/dx = y0/x0
(2) 计算要withdraw的dx和dy:

计算移除dxdy计算移除dxdy

(3) 补充证明公式:
(L0-L1)/L0 = dx/x0=dy/y0

步骤:

公式推导1公式推导1

公式推导2公式推导2

5.2.2 移除流动性-流程图

移除流动性流程图移除流动性流程图

niswapV2-移除流动性niswapV2-移除流动性

画板连接

5.3 Swap兑换

5.3.1 swap算法

(1) AMM算法公式

swap算法withoutFeeswap算法withoutFee

加上swap Fee 之后:

swap算法withFeeswap算法withFee

(2) 计算SpotSprice 当前价格

uniswapV2_当前价格和执行价格的关系uniswapV2_当前价格和执行价格的关系

(3) 产生slippage 滑点

定义:滑点就是你期望获得的和实际得到的价格差

uniswapV2_滑点uniswapV2_滑点

滑点产生原因:

uniswapV2_滑点产生原因uniswapV2_滑点产生原因

5.3.2 swap多跳

uniswapV2_多跳swapuniswapV2_多跳swap

5.3.2 总结swap流程图

画板链接

6. UniSwap 的闪电贷

6.1 Uniswap V2 闪电兑换概述

Uniswap V2 中的 闪电兑换(Flash Swap)是一种强大的功能,允许用户无需提前准备资产即可执行代币兑换,并且只要在同一笔交易中偿还所借资产或等价资产,用户就不需要支付抵押。这一功能是 Uniswap V2 的扩展,相当于允许用户从流动性池中临时借出资金,用于套利、抵押或其他复杂的 DeFi 操作。

从代码层面来说,闪电兑换的触发在 UniswapV2Pair 合约的 swap 函数里的,该函数里有这么一行代码:

if (data.length >0) lUniswapV2Callee(to).uniswapV2Call(msg.sender, amountOOut, amount1Out, data);

这行代码主要说明了三个信息:

  • to 地址是一个合约地址
  • to 地址的合约实现了 IUniswapV2Callee 接口
  • 可以在 uniswapV2Call 函数里执行 to 合约自己的逻辑

一般情况下的兑换流程,是先支付 tokenA 再得到 tokenB 但闪电兑换却可以先得到 tokenB 最后再支付 tokenA|tokenB。如下图:

uniswapV2_闪电贷流程uniswapV2_闪电贷流程

6.2 闪电兑换流程

闪电贷合约流程闪电贷合约流程

6.2.1 借出代币

用户调用

swap
函数时,可以借出
amount0Out
和/或
amount1Out
数量的代币。此时,用户无需提前提供资金抵押,但必须在同一笔交易内通过回调函数
UniswapV2Call
来完成相应的操作。

pair.swap(amount0Out, amount1Out, to, data);
  • 如果
    data
    不为空,Uniswap V2 会理解为这是一次闪电兑换,并在取出代币后调用用户实现的回调函数
    UniswapV2Call

6.2.2 回调函数
UniswapV2Call

swap
函数被执行并检测到
data
不为空时,合约会调用一个回调函数
UniswapV2Call
,允许用户在这个过程中执行自定义逻辑。这就是闪电兑换的核心步骤。

function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external {
    // 执行自定义的闪电兑换逻辑
}
  • sender
    : 交易的发起者地址。
  • amount0
    : 借出的
    token0
    的数量。
  • amount1
    : 借出的
    token1
    的数量。
  • data
    : 交易时传入的自定义数据(通常包含额外的逻辑指令)。

6.2.3 偿还借款

uniswapV2Call
回调中,用户可以执行任何复杂的逻辑,比如在其他交易所套利、借贷、清算等操作。然而,在交易结束时,用户必须确保按照 Uniswap 的恒定乘积公式偿还所借代币:

  • 直接偿还借款:用户可以通过将借出的代币在回调中直接发送回 Uniswap 流动性池来完成闪电兑换。
  • 以另一种代币偿还:用户可以通过使用借来的代币在同一笔交易中在其他地方兑换出另一种代币,然后用该代币偿还流动性池。例如,如果用户借出的是
    token0
    ,那么他们可以在回调中卖掉
    token0
    兑换成
    token1
    ,并偿还
    token1

偿还借款时,还需要支付 0.3% 的手续费。如果没有按照要求偿还所借代币,整个交易会回滚。

6.2.4 计算闪电贷成功最低费率

公式:

闪电贷成功费率公式闪电贷成功费率公式

推导过程:

闪电贷公式推导过程闪电贷公式推导过程

闪电贷推导过程2闪电贷推导过程2

6.2.5 Uniswap v2 闪电兑换案例

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {IUniswapV2Pair} from
    "../../../src/interfaces/uniswap-v2/IUniswapV2Pair.sol";
import {IERC20} from "../../../src/interfaces/IERC20.sol";

error InvalidToken();
error NotPair();
error NotSender();

contract UniswapV2FlashSwap {
    IUniswapV2Pair private immutable pair;
    address private immutable token0;
    address private immutable token1;

    constructor(address _pair) {
        pair = IUniswapV2Pair(_pair);
        token0 = pair.token0();
        token1 = pair.token1();
    }

    function flashSwap(address token, uint256 amount) external {
        if (token != token0 && token != token1) {
            revert InvalidToken();
        }

        // 1. Determine amount0Out and amount1Out
        (uint256 amount0Out, uint256 amount1Out) =
            token == token0 ? (amount, uint256(0)) : (uint256(0), amount);

        // 2. Encode token and msg.sender as bytes
        bytes memory data = abi.encode(token, msg.sender);

        // 3. Call pair.swap
        pair.swap({
            amount0Out: amount0Out,
            amount1Out: amount1Out,
            to: address(this),
            data: data
        });
    }

    // Uniswap V2 callback
    function uniswapV2Call(
        address sender,
        uint256 amount0,
        uint256 amount1,
        bytes calldata data
    ) external {
        // Write your code here
        // Don’t change any other code

        // 1. Require msg.sender is pair contract
        // 2. Require sender is this contract
        // Alice -> FlashSwap ---- to = FlashSwap ----> UniswapV2Pair
        //                    <-- sender = FlashSwap --
        // Eve ------------ to = FlashSwap -----------> UniswapV2Pair
        //          FlashSwap <-- sender = Eve --------
        if (msg.sender != address(pair)) {
            revert NotPair();
        }
        // 2. Check sender is this contract
        if (sender != address(this)) {
            revert NotSender();
        }
        // 3. Decode token and caller from data
        (address token, address caller) = abi.decode(data, (address, address));
        // 4. Determine amount borrowed (only one of them is > 0)
        uint256 amount = token == token0 ? amount0 : amount1;

        // 5. Calculate flash swap fee and amount to repay
        // fee = borrowed amount * 3 / 997 + 1 to round up
        uint256 fee = ((amount * 3) / 997) + 1;
        uint256 amountToRepay = amount + fee;

        // 6. Get flash swap fee from caller
        IERC20(token).transferFrom(caller, address(this), fee);
        // 7. Repay Uniswap V2 pair
        IERC20(token).transfer(address(pair), amountToRepay);
    }
}

7. UniSwap 的Twap(时间加权平均价格)

  • TWAP 全称是 时间加权平均价格
  • 核心理念:不是使用某个瞬间的现货价格,而是计算一段时间内的平均价格。这能有效平滑价格,抵御短期市场操纵(例如闪电贷攻击)和巨量单笔交易造成的价格波动。

7.1 公式推导

uniswapV2_twapuniswapV2_twap

uniswapV2_twap公式推导uniswapV2_twap公式推导

7.2 Cumulative price

twap公式推导3twap公式推导3

twap公式推导4twap公式推导4

V2代码: update 函数更新price0CumulativeLast,price1CumulativeLast和reserve0,reserve1

 // update reserves and, on the first call per block, price accumulators
 // NOTE: swap/add liquidity/remove liquidity will call this function to update reserves

function _update(uint256 balance0, uint256 balance1, uint112 _reserve0, uint112 _reserve1) private {
       require(balance0 <= uint112(-1) && balance1 <= uint112(-1), "UniswapV2: OVERFLOW");
       uint32 blockTimestamp = uint32(block.timestamp % 2 ** 32);
       uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired

       if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
           // * never overflows, and + overflow is desired
            // NOTE: TWAP is time-weighted average pricing
            // * never overflows, and + overflow is desired
            // *不会发生溢出,且+溢出是期望的
     //                                        224 bits   *   32 bits = 256 bits
					price0CumulativeLast += uint256(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
          price1CumulativeLast += uint256(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
					}
       reserve0 = uint112(balance0);
       reserve1 = uint112(balance1);
       blockTimestampLast = blockTimestamp;
       emit Sync(reserve0, reserve1);
    }

1. 价格累加器 - 核心数据结构

在每一个 Uniswap V2 交易对的合约中,都存储着两个关键状态变量:

  • price0CumulativeLast
  • price1CumulativeLast

它们是什么? 这不是一个瞬时价格,而是一个自交易对创建以来,价格随时间的积分值(累加和)

如何更新? 在每一次交易(

swap
)、铸造(
mint
)、销毁(
burn
)之前,合约都会执行以下操作:

  1. 计算当前区块时间戳
    block.timestamp
    与上一次更新时间戳
    blockTimestampLast
    的时间差。
  2. 获取当前 reserves 计算出的现货价格
    price
  3. 价格 * 时间差
    累加到
    priceCumulativeLast
    上。
  4. 更新 reserves 和
    blockTimestampLast

公式简化表示:

priceCumulativeLast += price * timeElapsed

2. 如何计算一个时间窗口的 TWAP?

假设一个外部合约(例如借贷协议、衍生品合约)需要过去 1 小时 的 TWAP。它会这样做:

  1. 首次读数:在触发时间窗口开始时,读取并记录:
    • priceCumulativeStart
    • timestampStart
  2. 二次读数:在触发时间窗口结束时(或任何需要价格的时刻),读取:
    • priceCumulativeEnd
    • timestampEnd
  3. 计算
    • 时间差 =
      timestampEnd - timestampStart
    • 累积价格差 =
      priceCumulativeEnd - priceCumulativeStart
    • TWAP = 累积价格差 / 时间差

计算示例:

twap举例twap举例

7.3 计算当前时间的TWAP

twap最终twap最终

最后更新: 2026年1月3日

评论区

发表评论

请先 登录 后再发表评论

请遵守相关法律法规,文明发言。评论将在审核后显示。

评论

还没有评论,来做第一个评论者吧!