手把手教你从0到1构建UniswapV2:part2

  • Louis
  • 发布于 2024-08-01 19:34
  • 阅读 2041

本篇文章,我们将向我们自己的UniswapV2源码中添加核心的功能——swapping。Uniswap创建的目的就是能够实现去中心化的代币交换,我们一起来看看它是如何完成的。我们会依然专注于核心的配对合约,我们还没开始构建可用的前端交互界面,也并不会进行价格计算。

简单介绍

欢迎回来,本篇文章,我们将向我们自己的UniswapV2源码中添加核心的功能——swapping。Uniswap创建的目的就是能够实现去中心化的代币交换,我们一起来看看它是如何完成的。我们会依然专注于核心的配对合约,我们还没开始构建可用的前端交互界面,也并不会进行价格计算。

此外,我们还将实现一个价格预言机:配对合约的设计允许我们仅仅用几行代码就能实现。

我们还会一起思考和讨论配对合约背后的一些设计细节和想法,这是我们在之前的文章中没有关注的。

代币交换

到目前为止,我们已经具备了执行代币交换的一切条件,让我们思考下如何去实现它。

代币的交换意味着放弃一定数量的代币A来换取代币B。但是在这个过程中,我们需要某种中介来做一些事情:

  • 1、提供实时汇率
  • 2、保证所有的交换都能够全额支付,即所有的交换都能够按照正确的汇率来进行。

当我们研究流动性供应时,我们了解了一些关于DEX定价的知识:流动性池子中的流动性数量决定了汇率。在Uniswap V1系列中,我们详细解释了恒定乘积公式是如何工作的以及成功交换的主要条件是什么。即:交换后的Token准备金的乘积必须等于或者大于交换前的准备金的乘积。无论流动性池子中的Token储备量是多少,恒定乘积必须保持不变,这是我们必须保证的唯一条件。

我们之前已经介绍过,配对合约是核心合约,这意味着它必须尽可能设计成一个基础合约和最小化。这会影响我们向合约发送代币的方式。有两种方式可以将代币转移给某人:

  • 1、通过调用代币合约的transfer方法并传递接收者的地址和要发送的金额。
  • 2、通过调用approve方法来允许其他用户或者合约将一定数量的代币转移到他们的地址。对方必须调用transferFrom才能获取你的代币。你作为代币的所有者需要批准一定的金额;另一方支付实际转账费用。

批准模式在以太坊的应用中很常见:dapp要求用户批准最大金额的支出,这样用户就不需要一次又一次的调用approve方法(这会花费Gas),批准模式确实提升了用户体验。

我们开始写代码吧:

这个swap函数有三个入参,前两个入参代表的是代币的输出量,这是调用者希望用他的代币换取的数量。为什么要这样设计呢?因为我们并不想强制约束swap的方向:调用者可以指定其中一个或者两个,我们只需要执行必要的检查就可以了。

function swap(
    uint256 amount0Out,
    uint256 amount1Out,
    address to
) public {
    if (amount0Out == 0 && amount1Out == 0)
        revert InsufficientOutputAmount();

    ...

接下来,我们需要确保有足够的Token储备发送给用户。

...

  (uint112 reserve0_, uint112 reserve1_, ) = getReserves();

  if (amount0Out > reserve0_ || amount1Out > reserve1_)
      revert InsufficientLiquidity();

...

接下来,我们计算该合约的代币余额减去我们预计发送给调用者的金额。此时,我们预计调用者已经将他们想要交易的Token代币发送到该合约中。因此,其中一个或者两个代币的余额预计将大于相应的代币储备数量。

我们需要持续的边界和异常检查,我们预计该合约的Token代币余额和储备金不同(余额很快会保存到储备金中),并且我们需要确保他们的乘积等于或者大于或者等于当前储备金的乘积,如果满足这个要求,则:

  • 1、调用者已经正确计算了汇率(包括滑点)。
  • 2、输出的金额是正确的。
  • 3、转入合约的数量也是正确的。
...
if (balance0 * balance1 < uint256(reserve0_) * uint256(reserve1_))
    revert InvalidK();
...

到这一步,可以安全的将代币转移给调用者并更新储备数量了,此时交换完成。

    _update(balance0, balance1, reserve0_, reserve1_);

    if (amount0Out > 0) _safeTransfer(token0, to, amount0Out);
    if (amount1Out > 0) _safeTransfer(token1, to, amount1Out);

    emit Swap(msg.sender, amount0Out, amount1Out, to);
}

提示:上面的这个实现并不完整:合约并没有收取交易费用,因此,流动性提供者无法从中获得收益,我们将在价格计算后完善这个部分。

防止重入攻击:

对以太坊智能合约最常见的攻击形式之一就是重入攻击。 当合约在没有进行必要的检查或者更新状态的情况下进行外部调用时,很可能就会发生这种攻击。攻击者可以欺骗合约调用攻击者的合约。而攻击者的合约又会再次调用被攻击的合约(通常会调用多次)。因次。第二次调用(重新进入合约)利用了合约状态的错误更新,从而导致资金损失(这是攻击的主要目标)。

在配对合约中,swap函数中有safeTransfer这个方法的调用——合约会将代币发送给调用者,重入攻击正是对这种调用进行攻击的。 如果我们总是假定被调用的transfer方法完全按照我们期望的方式执行,这种想法是很天真的。ERC20的代币标准并不是那么强制,开发人员可以根据需求定制任何想要做的事情。

防止重入攻击的常见方式有两种:

使用"可重入防护"

我们可以使用比较成熟的实现,比如 OpenZeppelin 合约中的 ReentrancyGuardUniswapV2 自己实现了一个,因为它并不难实现,主要的思想是在调用函数时候设置一个标志,设置了这个标志后这个函数就不允许被调用了;等到函数被调用完成之后这个标志将被取消。

这种机制不允许在调用函数时同一个用户重复调用该函数,但是也并不会影响其他的用户。每个用户之间行为都是独立的。

根据代码来介绍下防止重入的实现原理

ReentrancyGuard 的实现原理如下:

状态变量:

ReentrancyGuard 定义了一个状态变量 _status,用于表示函数调用的状态。这个变量可以有两个值:_NOT_ENTERED_ENTERED。默认情况下,合约处于 _NOT_ENTERED 状态。

uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;
uint256 private _status;

构造函数:

在构造函数中,_status 被初始化为 _NOT_ENTERED

constructor() {
    _status = _NOT_ENTERED;
}

修饰器:

关键在于 nonReentrant 修饰器。这个修饰器在函数执行之前会检查 _status 是否为 _NOT_ENTERED,如果不是,就会抛出异常,阻止函数执行。执行函数之前将 _status 设置为 _ENTERED,函数执行完毕后再将其设置回 _NOT_ENTERED

modifier nonReentrant() {
    require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
    _status = _ENTERED;
    _;
    _status = _NOT_ENTERED;
}

使用方法:

在需要防止重入攻击的函数上,使用 nonReentrant 修饰器。

contract MyContract is ReentrancyGuard {
    function safeFunction() external nonReentrant {
        // function logic
    }
}

safeFunction 被调用时,nonReentrant 修饰器会首先检查 _status 是否为 _NOT_ENTERED,如果是,则继续执行并将 _status 设置为 _ENTERED。这样,任何在函数执行期间的重入调用都会被阻止,因为 _status 不再是 _NOT_ENTERED。函数执行完毕后,_status 会被恢复为 _NOT_ENTERED,以便后续调用。

遵循检查、效果、交互模式

该模式在合约函数中强制执行严格的操作顺序:首先,进行所有必要的检查,从而确保该功能在正确的状态下工作。其次,函数根据其逻辑更新自身的状态。最后,该函数进行外部调用。(更新自身状态放在外部调用之前),这样的顺序保证了每次函数调用都是在函数状态最终确定且正确时进行的。不存在待处理的状态需要更新了。

我们的swap实现容易遭受到攻击吗?可以欺骗它其中的储备资产发送给调用者吗?从理论上讲是的,因为它依赖于第三方合约(token合约),并且任何一个代币合约都可以为它提供错误的余...

剩余50%的内容订阅专栏后可查看

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Louis
Louis
web3 developer,技术交流或者有工作机会可加VX: magicalLouis