LOADING

加载过慢请开启缓存 浏览器默认开启

Decentralized Exchange Development 101: From Zero to One (3)

2023/6/27 Solidity Coding

AMM

AMM是自动做市商的缩写,它是一种通过自动算法交易提供流动性的协议。AMM可以在去中心化交易所(DEX)上为不同的代币创建和运行可公开获取的链上流动性。

AMM的基本原理是使用一个恒定函数来确定两种代币之间的价格,这个函数通常被称为交易池流动性池。交易池由两种代币组成,比如ETH和DAI,它们按照一定的比例存入池中。交易池的总价值始终保持不变,这就是恒定函数的含义。

例如,假设有一个交易池由100个ETH和20000个DAI组成,那么它的总价值就是100 x 200 + 20000 = 40000美元(假设1 ETH = 200美元)。如果有人想用10个ETH换取一些DAI,那么他就需要向池中存入10个ETH,并从池中取出一些DAI,使得池中的总价值仍然等于40000美元。根据恒定函数,我们可以计算出他能够取出的DAI的数量为:

解得:

也就是说,他能够用10个ETH换取1818.18个DAI,相当于每个ETH的价格为181.82美元。这个价格低于市场价格,因为他向池中增加了ETH的供应,从而降低了ETH的相对价值。

AMM的优点是它可以实现去中心化、无需许可、低摩擦和高效率的交易。AMM的缺点是它可能存在滑点、无常损失、前端攻击等风险。

AMM的一个典型例子是Uniswap,它是一个基于以太坊的DEX,使用了x * y = k这样一个简单的恒定函数来确定两种代币之间的价格,其中x和y分别表示池中两种代币的数量,k是一个常数。

Uniswap的核心代码如下:

pragma solidity ^0.5.0;

contract UniswapExchange {
    // Address of ERC20 token traded on this exchange
    address public token;

    // Provide liquidity and get pool tokens
    function addLiquidity(uint256 min_liquidity, uint256 max_tokens, uint256 deadline) public payable returns (uint256);

    // Burn pool tokens and get ERC20 tokens + ETH
    function removeLiquidity(uint256 amount, uint256 min_eth, uint256 min_tokens, uint256 deadline) public returns (uint256, uint256);

    // Trade ETH for ERC20 tokens
    function ethToTokenSwapInput(uint256 min_tokens, uint256 deadline) public payable returns (uint256);

    // Trade ERC20 tokens for ETH
    function tokenToEthSwapInput(uint256 tokens_sold, uint256 min_eth, uint256 deadline) public returns (uint256);
}

这里只展示了四个主要的函数,分别用于添加流动性、移除流动性、用ETH换取代币和用代币换取ETH。每个函数都有一些参数和返回值,比如最小流动性、最大代币、截止时间等。

AMM有很多不同的应用案例,这是一些例子:

  • 流动性挖矿:流动性挖矿是指用户向AMM提供流动性,并获得交易费用和代币奖励的过程。这是一种激励用户参与AMM的机制,也是DeFi领域中最受欢迎的活动之一。例如,用户可以向Uniswap的交易池中存入ETH和DAI,然后获得UNI代币作为奖励。
  • 动态做市商:动态做市商是指一种能够根据市场情况自动调整流动性的AMM。这种AMM可以提高资金利用率,降低滑点和无常损失,提高交易效率。例如,Bancor是一个动态做市商,它使用了一个名为BNT的代币作为所有交易池的中间媒介,从而实现了单边提供流动性和自动平衡的功能。
  • 高级AMM应用:高级AMM应用是指一种能够在每次交易后向特定的智能合约发送信号,并根据信号进行流动性调整的AMM。这种AMM可以实现更精确和高效的流动性管理,也可以支持更多的创新和定制化。例如,Arrakis是一个高级AMM应用,它使用了一个名为ALPHA的代币作为信号发送者,从而实现了基于风险调整的流动性分配和优化。

Uniswap

Uniswap是一个基于以太坊的去中心化交易所,使用了自动做市商(AMM)的机制来提供流动性和交易。Uniswap从2018年开始发展,目前已经推出了四个版本,每个版本都有自己的特点和优势。

  • Uniswap V1:这是Uniswap的第一个版本,于2018年11月在Devcon 4上发布。它使用了一个简单的恒定函数来确定两种ERC-20代币之间的价格,即x * y = k,其中x和y分别表示池中两种代币的数量,k是一个常数。用户可以向池中存入两种代币,并获得一种名为UNI-V1的流动性代币,代表他们在池中的份额。用户也可以用一种代币换取另一种代币,从而改变池中的代币比例和价格。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract UniswapV1 {
    // ERC-20 token A
    address public tokenA;
    // ERC-20 token B
    address public tokenB;
    // constant product k
    uint256 public k;

    // get the price of token A in terms of token B
    function getPriceA() public view returns (uint256) {
        // get the balances of token A and token B in the pool
        uint256 x = IERC20(tokenA).balanceOf(address(this));
        uint256 y = IERC20(tokenB).balanceOf(address(this));
        // calculate the price using the constant function formula
        return y * 1e18 / x; // multiply by 1e18 to get 18 decimals precision
    }

    // get the price of token B in terms of token A
    function getPriceB() public view returns (uint256) {
        // get the balances of token A and token B in the pool
        uint256 x = IERC20(tokenA).balanceOf(address(this));
        uint256 y = IERC20(tokenB).balanceOf(address(this));
        // calculate the price using the constant function formula
        return x * 1e18 / y; // multiply by 1e18 to get 18 decimals precision
    }
}
  • Uniswap V2:这是Uniswap的第二个版本,于2020年5月发布。它在V1的基础上增加了一些新功能,如直接的ERC-20/ERC-20交易池、闪电交换、以及改进的价格预言机。直接的ERC-20/ERC-20交易池可以让用户在不经过ETH的情况下交换两种ERC-20代币,从而节省了一次交易费用。闪电交换可以让用户借用池中的资金进行任意复杂的操作,只要在同一个事务中归还就行。改进的价格预言机可以让智能合约获取池中每次交易后的价格,从而防止操纵和攻击。

Uniswap V2使用了一个稍微不同的恒定函数来确定两种代币之间的价格,即x * y = k * (1 - fee),其中fee是每笔交易收取的费率(默认为0.003)。这个函数可以用下面的Solidity代码实现:

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol";

contract UniswapV2 {
    // ERC-20 token pair
    address public pair;
    // fee rate per swap (default is 0.003)
    uint256 public fee = 0.003e18; // multiply by 1e18 to get 18 decimals precision

    // get the price of token 0 in terms of token 1
    function getPrice0() public view returns (uint256) {
        // get the reserves of token 0 and token 1 in the pair
        (uint112 reserve0, uint112 reserve1,) = IUniswapV2Pair(pair).getReserves();
        // calculate the price using the constant function formula
        return reserve1 * 1e18 / (reserve0 * (1e18 - fee)); // multiply and divide by 1e18 to get 18 decimals precision
    }

    // get the price of token 1 in terms of token 0
    function getPrice1() public view returns (uint256) {
        // get the reserves of token 0 and token 1 in the pair
        (uint112 reserve0, uint112 reserve1,) = IUniswapV2Pair(pair).getReserves();
        // calculate the price using the constant function formula
        return reserve0 * 1e18 / (reserve1 * (1e18 - fee)); // multiply and divide by 1e18 to get 18 decimals precision
    }
}
  • Uniswap V3:这是Uniswap的第三个版本,于2021年5月发布。它是目前最先进的AMM方案之一,注重资本效率的最大化。它引入了以下几个新特性:
    • 集中的流动性:这个特性可以让流动性提供者自由地选择他们想要提供流动性的价格区间,而不是像之前那样提供整个价格曲线上的流动性。这样可以提高流动性提供者的收益率和资金利用率,也可以降低交易者的滑点和无常损失。
    • 多级费率控制:这个特性可以让流动性提供者根据不同交易对的风险和需求选择不同的费率档位(0.05%、0.3%或1%),从而平衡收益和竞争力。
    • 范围订单:这个特性可以让流动性提供者将他们想要买卖某种代币的价格区间作为流动性添加到池中,从而实现被动挂单和主动做市的结合。
    • 历史预言机:这个特性可以让智能合约获取池中任意时间点的价格,从而支持更多复杂和创新的应用场景。

Uniswap V3使用了一个完全不同的恒定函数来确定两种代币之间的价格,即x * y = k * sqrtPriceX962,其中x和y分别表示池中两种代币的数量,k是一个常数,sqrtPriceX96是一个96位整数,表示两种代币之间价格平方根的乘以296后取整的值。这个函数可以用下面的Solidity代码实现:

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";

contract UniswapV3 {
    // ERC-20 token pair
    address public pair;
    // fee rate per swap (default is 0.003)
    uint24 public fee = 3000; // multiply by 1e6 to get 6 decimals precision

    // get the price of token 0 in terms of token 1
    function getPrice0() public view returns (uint256) {
        // get the current sqrtPriceX96 from the pool
        (uint160 sqrtPriceX96,,,,,,) = IUniswapV3Pool(pair).slot0();
        // calculate the price using the constant function formula
        return uint256(sqrtPriceX96)**2 / 2**192; // divide by 2**192 to get 18 decimals precision
    }

    // get the price of token 1 in terms of token 0
    function getPrice1() public view returns (uint256) {
        // get the current sqrtPriceX96 from the pool
        (uint160 sqrtPriceX96,,,,,,) = IUniswapV3Pool(pair).slot0();
        // calculate the price using the constant function formula
        return 2**192 / uint256(sqrtPriceX96)**2; // divide by 2**192 to get 18 decimals precision
    }
}

在Uniswap V3中,slot和tick是两个重要的概念,它们与集中的流动性和价格预言机有关。

  • slot:slot是一个存储结构,用于记录池中每个tick的状态。每个slot包含以下几个字段:
    • liquidityNet:表示该tick上流动性的净变化,即进入该tick的流动性减去离开该tick的流动性。
    • liquidityGross:表示该tick上流动性的总量,即进入该tick的流动性加上离开该tick的流动性。
    • feeGrowthOutside0X128:表示该tick之外的token 0累积的费用增长率,用于计算流动性提供者的收益。
    • feeGrowthOutside1X128:表示该tick之外的token 1累积的费用增长率,用于计算流动性提供者的收益。
    • secondsOutside:表示该tick之外的累积秒数,用于计算流动性提供者的收益。
    • secondsPerLiquidityOutsideX128:表示该tick之外的每单位流动性累积秒数,用于计算流动性提供者的收益。
    • initialized:表示该tick是否已经被初始化,即是否有流动性提供者选择了该tick作为他们的价格区间边界。

slot可以用下面的Solidity代码实现:

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

import "@uniswap/v3-core/contracts/libraries/TickMath.sol";

struct Slot {
    // the total amount of position liquidity that references this tick
    uint128 liquidityGross;
    // amount of net liquidity added (subtracted) when tick is crossed from left to right (right to left),
    int128 liquidityNet;
    // fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick)
    // only has relative meaning, not absolute — the value depends on when the tick is initialized
    uint256 feeGrowthOutside0X128;
    uint256 feeGrowthOutside1X128;
    // the cumulative tick value on the other side of the tick
    int56 tickCumulativeOutside;
    // the seconds per unit of liquidity on the _other_ side of this tick (relative to the current tick)
    // only has relative meaning, not absolute — the value depends on when the tick is initialized
    uint160 secondsPerLiquidityOutsideX128;
    // the seconds spent on the other side of the tick (relative to the current tick)
    // only has relative meaning, not absolute — the value depends on when the tick is initialized
    uint32 secondsOutside;
    // whether the tick is initialized, i.e. the value is exactly equivalent to the expression liquidityGross != 0
    // these 8 bits are set to prevent fresh sstores when crossing newly initialized ticks
    bool initialized;
}
  • tick:tick是一个整数,表示两种代币之间价格平方根的乘以296后取整的值在一个等差数列中的位置。每个tick之间相差60个单位,即260。每个tick都对应一个slot,用于记录该tick上的流动性和费用信息。

例如,假设有一个交易池由ETH和USDC组成,那么它们之间价格平方根的乘以296后取整的值可以表示为sqrtPriceX96。如果sqrtPriceX96等于1.0001 * 296,则对应的tick为0。如果sqrtPriceX96等于1.0001 * 2156,则对应的tick为1。如果sqrtPriceX96等于1.0001 * 2216,则对应的tick为2,以此类推。

tick可以用下面的Solidity代码实现:

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

import "@uniswap/v3-core/contracts/libraries/TickMath.sol";

// get the tick corresponding to a given sqrtPriceX96
function getTickAtSqrtRatio(uint160 sqrtPriceX96) internal pure returns (int24) {
    // calculate the log base 1.0001 of the ratio
    uint256 ratio = uint256(sqrtPriceX96) << 32;
    uint256 log_2 = BitMath.mostSignificantBit(ratio);
    int24 tick = int24((log_2 - 128) << 3);

    // round to the closest multiple of 60
    int256 remainder = (int256(ratio) >> (log_2 - 7)) - (int256(1) << (8 + 3));
    if (remainder < 0) {
        remainder = -remainder;
        if (remainder >= int256(1) << (8 + 3 - 1)) {
            tick -= 60;
        }
    } else {
        if (remainder > int256(1) << (8 + 3 - 1)) {
            tick += 60;
        }
    }

    // check the tick is within the range
    require(tick >= TickMath.MIN_TICK && tick <= TickMath.MAX_TICK, "T");
    return tick;
}

在Uniswap V3中,流动性是一个重要的概念,它表示池中两种代币之间的交易能力。流动性由流动性提供者(LP)提供,他们可以选择一个价格区间,将两种代币存入池中,并获得一种名为UNI-V3的流动性代币,代表他们在池中的份额。

流动性的计算方法取决于当前价格是否在LP选择的价格区间内。如果是,那么流动性等于两种代币提供的最小流动性;如果不是,那么流动性等于超出价格区间边界的一种代币提供的流动性。

我们用表示池中两种代币的数量,用表示当前价格(即兑换的比率),用表示价格区间的下限和上限,用表示流动性。那么,根据Uniswap V3白皮书中的公式,我们可以得到以下几种情况:

  • 如果,即当前价格在价格区间内,那么流动性等于
  • 如果,即当前价格低于价格区间下限,那么流动性等于
  • 如果,即当前价格高于价格区间上限,那么流动性等于

这些公式可以用下面的Solidity代码实现:

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";

contract UniswapV3Liquidity {
    // ERC-20 token pair
    address public pair;
    // price range lower bound (in terms of token 0)
    uint160 public pa;
    // price range upper bound (in terms of token 0)
    uint160 public pb;

    // get the liquidity of a position within the price range
    function getLiquidity() public view returns (uint128) {
        // get the current sqrtPriceX96 and the reserves of token 0 and token 1 from the pool
        (uint160 sqrtPriceX96, uint128 liquidity, , , , , ) = IUniswapV3Pool(pair).slot0();
        (uint256 reserve0, uint256 reserve1) = IUniswapV3Pool(pair).observe(0);
        // calculate the current price (in terms of token 0)
        uint256 p = uint256(sqrtPriceX96)**2 / 2**192;
        // calculate the liquidity according to different cases
        if (p >= pa && p <= pb) {
            // case 1: current price is within the price range
            return liquidity;
        } else if (p < pa) {
            // case 2: current price is below the price range lower bound
            return uint128(reserve0 / pa - reserve1 / p);
        } else {
            // case 3: current price is above the price range upper bound
            return uint128(reserve1 / pb - reserve0 / p);
        }
    }
}
  • Uniswap V4:这是Uniswap即将推出的第四个版本,目前还没有正式发布。根据开发团队透露,它将会在Optimism上部署,从而利用其第二层扩容方案来降低交易费用和提高吞吐量。

顺带推荐一个非常完善的解释Uniswap V3的教程:https://y1cunhui.github.io/uniswapV3-book-zh-cn/