# 独立复现一次DEFI攻击需要准备一些什么？——uniswap与AMM


By [zxzyy10](https://paragraph.com/@zxzyy10) · 2022-09-05

---

1.  自动做市商模型是去中心化交易所（DEX）的核心技术。关于做市商模型的讨论网络上有较多广泛但不深刻的讨论，从根本上来说，DEFI是金融领域在区块链系统上完成基本记账功能后进一步探索性的应用。对于有过传统做市商模型下交易经验的学习者，可能可以较为顺利地理解传统交易模型中做市商存在的意义以及区块链系统中去中心化交易所（DEX）采用的自动做市商模型（AMM）的与前者的差别。对于没有相关经验或者理解不那么深刻的计算机行业从业人员来说，回过头去学习一些基本概念是必要的，这并不会花费太多的时间。
    
2.  自动做市商模型种类有很多 ，在此我们选择uniswap采用的恒定乘积做市商模型来了解最初DEFI基础业务的整个业务流程以及相关概念。关于恒定乘积做市商模型，笔者的理解是这是对商品供求关系以及价格的简单建模，在这个模型下，有几个要点是读者需要注意和理解的。
    
    1.  模型中存在的身份有流动性提供者(LP)以及正常的交易者，流动性提供者与正常交易者在利用去中心化交易所（DEX）完成自己特定的金融行为时有不同的注意事项。
        
    2.  对于流动性提供者而言，为交易池提供流动性是其主要的行为，在最初的DEFI业务中，流动性提供者为交易池提供流动性需要同时持有两种代币，通常而言这两种代币分别是以太坊原生代币ETH以及其他一类ERC20代币，首个为交易池添加流动性的流动性提供者可以按任意的比例添加两种代币，后续的流动性提供者需要按照自己提供流动性时交易池中两种代币的比例等比例添加流动性。例如：首次提供流动性时代币比例为1:200，而经过几次交易后，代币比例成为了4:50，那么在这个时刻，如果有流动性提供者想为交易池提供流动性，就需要按4:50的比例来提供。在最初的恒定乘积做市商模型下，流动性提供者需要承受代币价格偏离流动性提供时的价格而带来的损失，称为无常损失，关于无常损失的计算，网络中已经出现了相关的计算工具，其计算方法这里不做赘述。关于流动性大小有一个定性的结论，当交易池中的流动性越小，我们进行一次交易带来的价格偏离就会越大，这其实也是符合供求关系对价格影响的直观判断的。
        
    3.  对于普通交易者而言，完成代币的兑换是其主要行为。而具体到交易的方法，则需要读者注意，不同于生活中的常规交易，在自动做市商模型下我们无法用统一的价格兑换到所有代币，例如：在代币A、B交易对的交易池中，当你看到交易池中的A代币价格为10U时，无法使用价格100U的B代币去换到10枚A代币。假设我们简单的从供求关系来理解恒定乘积做市商模型，那么在交易过程中，购买这一动作会影响供求关系使得A、B的相对价格发生变化，无法稳定在最初的恒定比例，通常来讲，用B代币对换A代币这一动作使得交易池中B代币的数量增多，A代币的数量减少，那么在恒定乘积的做市商模型下，A的价格升高了，A、B两种代币的相对价格发生变化，这一情况使得交易者经过兑换后能换取的A代币减少了，换不到10枚，具体能换到多少，需要经过计算以后才能得到最终结果，这便是交易者要面对的滑点问题。正因如此，在进行ETH和其他ERC20代币的兑换时，DEFI业务中交易者可能会面对以下几种交易情况。
        
        1.  用定量的ETH换取ERC20代币。
            
        2.  用ETH换取定量的ERC20代币。
            
        3.  用定量的ERC20代币换ETH。
            
        4.  用ERC20代币换取定量的ETH。
            

而以上情况在uniswap V1中对应的交易方法分别为 1、 ethToTokenSwapInput

![](https://storage.googleapis.com/papyrus_images/385b954846c3321e3d41570a1984fb5a595d30db53407d6b532d8a549ff09466.png)

2、 ethToTokenSwapOutput

![](https://storage.googleapis.com/papyrus_images/0dd709baa4df7d809b008d44098bc0e0f4febb3b80a39dc31706ec5ad4fa516e.png)

3、 tokenToEthSwapInput

![](https://storage.googleapis.com/papyrus_images/9353907e4901119b01f6ddfff2f19f4f4fa575f061e6d8af4fa2cc7205b7fd1e.png)

4、 tokenToEthSwapOutput

![](https://storage.googleapis.com/papyrus_images/0c29ecd24175af6551b00c5d0e33a98f423af0ba1862ae89fa234b4b9fb4378a.png)

这4个方法在初看时很容易混淆，这里我们将以上四个方法再统一一下。总的来说有两种兑换方式，这一点是易于理解的，一种是用ETH换取ERC20代币，另一种是用ERC20代币换取ETH，对应于方法名中的ehtToToken以及tokenToEth，如何理解方法名中的Input以及Output呢。

在所有包含Input的交易方法中，交易者可以规定具体要放入交易池中的代币数量，例如在用ETH兑换ERC20代币的交易过程中，可以规定一次交易放入池中的ETH数量，而返回的ERC20代币数量是需要根据AMM模型进行计算的，反过来，在使用ERC20代币对换ETH的交易过程中，交易者可以规定一次交易放入池中的ERC20代币数量，此时返回的是根据AMM模型计算的能兑换的ETH的数量。

在包含 Output的交易方法中，交易者则可以规定要从交易池中兑换出的代币数量，例如在用ETH兑换ERC20代币的过程中，规定要兑换出的ERC20代币，所需要花费的ETH数量由AMM模型计算后返回，使用ERC20代币对换ETH时也是一样的道理。

当交易者使用ethToTokenSwapInput方法用ETH换取ERC20代币时，交易者需要设置本次交易想出售的ETH数量，以及可以接受的被兑换的ERC20代币的最小数量，只有当交易池中的流动性能够满足本次交易的设置参数时交易才能成功。

例如，如果交易池中的流动性较差，少量的购买即带来较大的价格波动，从而使得能兑换的ERC20代币数量小于交易者设置的参数值，那么此次交易不会被成功执行。 同理当交易者使用ethToTokenSwapOutput方法用ETH换取ERC20代币时，交易者需要设置本次交易想要购买的ERC20代币数量，以及可以接受的购买这些ERC20代币所需要花费的最大数量ETH，只有当本次交易能满足参数设置时交易才能被成功执行。

uniswap V1提供了四类对应于上述交易方法的价格查询方法来满足交易者的价格预估需求，如何计算价格，或者说如何在当前情况下计算

1） 交易者设置的ETH数量能换取多少ERC20代币

2） 交易者想换取定量的ERC20代币需要花费多少数量的ETH

3） 交易者设置的ERC20代币数量能换取多少数量的ETH

4） 交易者想换取定量的ETH需要花费多少数量ERC20代币

在此先简述一下计算流程，对于恒定乘积做市商模型，假设池子中的A、B交易对的代币数量为X，Y（这里假设A为ETH，B为某ERC20代币），则X、Y需要满足

                              X*Y=K 
    

这一恒定乘积关系。 某一时刻我们假设交易池中X=10，Y=100，此时有一交易者想投入5个ETH来购买ERC20代币，请问该交易者能得到多少ERC20代币。计算步骤为：

1） 由于规定了ETH的投入数量，那么交易后交易池中ETH数量是可以确定的X的值为10+5=15

2） 在交易完成前，交易池内的K值为X交易前Y交易前=10100=1000，交易前后K值应保持不变为依旧为1000

3） 此时已知交易后的X值为15，那么Y交易后=K/X交易后=1000/15=66.67，能够换取的ERC20代币的数量为Y交易前-Y交易后=100-66.67=33.33个。

从计算中可以看到，当交易池中的ETH、ERC20代币相对价格看起来用1个ETH能换取10个ERC20代币时，在AMM模型下，5个ETH只能换取33.33个ERC20代币，比想象中少的16.67个ERC20代币即为交易者承受的滑点损失。

这种交易具体对应到的方法为ethToTokenSwapInput，如果在参数设置中交易者设置的最少ERC20代币的购买数量小于33.33，那么交易可以被成功执行，反之则失败退回。

反过来，如果想知道，购买50个ERC20代币需要花费多少ETH，则计算过程如下

1）由于规定了ERC20代币的购买数量，那么交易后交易池中ERC20代币数量是可以确定的Y的值为100-50=50

2）在交易完成前，交易池内的K值为X交易前Y交易前=10\*100=1000，交易前后K值应保持不变为依旧为1000

3） 此时已知交易后的Y值为15，那么X交易后=K/Y交易后=1000/50=20，需要花费的ETH的数量为Y交易后-Y交易前=20-10=10个。

此时采用的具体交易方法为ethToTokenSwapOutput，如果交易者在设置参数时设置的最大ETH数量大于10个，则本次交易可以被成功执行。

同样的，假设在交易池中X=10，Y=100时，交易者想出售50个ERC20代币至交易池，那么请问交易能获取多少数量ETH。计算步骤为：

1）由于规定了ERC20代币的出售数量，那么交易后交易池中ERC20代币数量是可以确定的Y的值为100+50=150

2）在交易完成前，交易池内的K值为X交易前Y交易前=10\*100=1000，交易前后K值应保持不变为依旧为1000

3）此时已知交易后的Y值为15，那么X交易后=K/Y交易后=1000/150=6.67，能够换取的ETH的数量为X交易前-X交易后=10-6.67=3.33个。

从计算中可以看到，当交易池中的ETH、ERC20代币相对价格看起来用10个ERC20代币能换取1个ETH时，在AMM模型下，50个ERC20代币只能换取3.33个ETH，比想象中少的1.67个ETH即为交易者承受的滑点损失。

这种交易具体对应到的方法为tokenToEthSwapInput，如果在参数设置中交易者设置的最少ETH的获取数量小于3.33，那么交易将被成功执行，反之则失败退回。

剩下一种tokenToEthSwapOutput交易方法这里就不举具体案例说明了，总而言之，计算过程中只有一个不变量就是乘积K，X、Y根据具体的调用的交易方法，X、Y中有一个值将被固定，而另一个值则可以被计算出来。在DEFI应用中，DEX收取的手续费需要被考虑到计算过程中。而在具体实现中，价格计算的方式可以归结为两种方法，一是通过规定input数量来计算返回的output的值，二是通过规定ouput的数量来计算返回的input的值。我们依次对两种方法进行分析

`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`

在这里我们还是从具体的假设开始，对于ETH与ERC20代币的交易池，ETH交易前后的数量设为X交易前、X交易后，ERC20代币交易前后的数量设为Y交易前、Y交易后。交易后ETH的数量增多，ERC20代币的数量减少，因此，getInputPrice方法中的三个参数分别可写为下式，

`input_amount=X交易后-X交易前=X交易后-input_reserve`

`input_reserve=X交易前，`

`output_reserve=Y交易前`

函数的返回值 numerator / denominator=Y交易前-Y交易后

对应到上文总结的计算方法，交易前后K值不变，

`Y交易后*X交易后=X交易前*Y交易前`

将X交易后除到等式右边

`Y交易后=X交易前*Y交易前/X交易后`

同时将上式中的X、Y用对应的参数替换

`Y交易后=input_reserve * output_reserve/(input_amount+input_reserve)`

`Y交易前-Y交易后 = output_reserve - input_reserve *output_reserve/(input_amount+input_reserve)`

`= output_reserve* input_amount / (input_amount+input_reserve)`

因为uniswap要收0.3%的手续费，因此input\_amount实际上要用扣除掉手续费的 input\_amount\_with\_fee来替换，正常情况下 `input_amount_with_fee=0.997*input_amount`，这里由于最后输出的结果分号上下同时放大1000倍结果不变，因此在

`input_amount_with_fee = 997*input_amount` 的情况下，有

`numerator: uint256 = input_amount_with_fee * output_reserve`

`denominator: uint256 = (input_reserve * 1000) + input_amount_with_fee`

返回值为两数之商。接下来看getOutputPrice方法

`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`

依照上文约定ETH交易前后的数量设为X交易前、X交易后，ERC20代币交易前后的数量设为Y交易前、Y交易后。当使用ETH兑换ERC20代币时，交易者设置想要换取的ERC代币数量，来计算需要花费多少ETH。

`output_amount = Y交易前-Y交易后=output_reserve-Y交易后`

`input_reserve=X交易前`

`output_reserve=Y交易前`

依然有

`Y交易后*X交易后=X交易前*Y交易前`

此时计算X交易后

`X交易后=X交易前*Y交易前/Y交易后`

将参数带入式中

`X交易后 =input_reserve*output_reserve/（output_reserve-output_amount）`

`X交易后-X交易前=input_reserve*output_reserve/(output_reserve-output_amount)-input_reserve`

`=input_reserve*output_amount/(output_reserve-output_amount)`

这里得出的是未交手续费之前需要的ETH数量，而如果要成功换取这么多ETH，在考虑手续费的情况下，应该用其除以0.997，即得到方法中的分子分母

`numerator: uint256 = input_reserve * output_amount * 1000`

`denominator: uint256 = (output_reserve - output_amount) * 997`

与getInputPrice方法不同，此处返回值为分子分母的商+1，即不可能在花费为0的情况下产生输出。

上文我们从自动做市商机制出发，说明了在AMM机制下一个建立在区块链系统之上的去中心化交易所中存在的两个身份——普通交易者与流动性提供者在系统中使用的主要行为动作，重点放在了普通交易者会频繁使用的的四种交易方式，对每种方式都举例进行了具体的交易计算。除此之外，uniswap V1中还提供了ERC20代币与ERC20代币的交换，但是ERC2之间的直接交换其实也是通过两次ETH与ERC20代币的交换来实现的，扎实掌握一次代币的基本原理即可。同时我们也能初步看到uniswap V1存在的问题，比如因滑点的存在导致代币之间的交换的资本利用效率不高，ERC20代币之间的交易由多次ETH与ERC20代币交易完成，交易者可能要承受多次滑点损失，这样一来资本利用效率更低了。

除此之外，交易池如何建立，建立后如何添加、移除流动性也是读者需要学习与关注的，在这里依旧利用uniswap V1做一个简单说明，通常来讲uniswap的使用者需要通过以下步骤来建立与自己有关的代币的交易池并为交易池提供相应的流动性。

1） 发布自己的ERC20代币合约。

2） 调用uniswap V1 Factory合约中的createExchange方法创建自己的交易池，createExchange中的参数即为自己发布的ERC20代币合约地址。

3） 调用createExchange方法后，将在链上创建出对应的Exchange合约并返回Exchange合约的地址。Exchange合约按照uniswap V1中Exchange template模板生成，Exchange template地址在uniswap V1初始化的时候就已经写死。

4） 得到Exchange合约地址后，交易池创建者就可以前往对应的Exchange合约调用addLiquidity方法为自己的代币首次添加流动性了（通常来讲首次添加流动性这个事情会由交易池的创建者来完成）。

5） 完成流动性的添加以后，流动性提供者会获得相应的流动性代币作为已提供流动性的凭证。在流动性提供者不再想提供流动性时候可以通过调用Exchange合约中removeLiquidity方法以销毁流动性代币的方式来移除属于自己的流动性，移除流动性的过程中将会根据该流动性提供者提供的流动性占总流动性的比例来返还收益以及对应的代币。

至此五个步骤通过uniswap完成了交易池的创建以及流动性的添加使得对应ERC20代币的Exchange合约能够完成ETH与ERC20代币兑换的功能。在整个过程中，使用者在创建交易池时需要先和Factory合约交互，Factory合约管理着所有使用uniswap创建交易池的交易对，提供根据ERC20代币合约地址查询对应的Exhange合约地址的功能，提供根据uniswap中Exchange的创建ID查找对应的Exchange合约地址的功能。通过和Factory合约交互调用合约中的createExchange方法来完成ERC20代币对应的Exchange合约创建，而Exchange合约的创建又需要用到Exchange template合约，在创建完对应的Exchange合约后通过调用合约中的方法完成流动性的添加，最终完成ETH与ERC20代币相互兑换的功能。

本期通过介绍uniswap与AMM机制，熟悉了交易者以及流动性提供者在以AMM为核心机制的DEX中会使用的具体交易方法以及流动性添加与移除的方法，熟悉了简单AMM中的价格计算过程，了解了从0到1创建交易池并为之提供流动性的过程，并借此知晓了Factory合约管理Exchange合约的方式。复现一次DEFI攻击了除了要找到基本的漏洞，还要知晓业务的具体流程并以此为复现攻击提供代币相关信息，复现一次攻击需要监测的变量通常来讲都是跨合约的，理顺合约之间的调用关系从而顺利有序地监测相关变量时保证攻击能够复现成功的关键。

除此之外，初期DEX采用简单AMM来实现功能，其存在的一些问题也需要被熟知，例如：

从流动性提供者的角度来看，需要同时提供两种代币才能完成流动性的添加，不仅要承受无常损失风险，还要承受两种代币带来的价格波动影响。从普通交易者的角度来看，交易时因为交易池流动性的不同可能要承受不同程度的滑点损失，资本利用率有待提高。

通常来说解决以上问题要需要引入新的机制或对使用场景作进一步限制，在探索过程中有一些已经在固定场景下被其他DEFI协议安全解决，而存在部分协议在尝试解决以上问题时并没有谨慎处理好业务逻辑，使得“黑客”能通过业务逻辑漏洞对相关协议发起攻击。熟悉初期DEFI的遗留问题，重点观察为解决旧问题而新引入的机制的具体实现，是复现攻击乃至发现新漏洞的一个突破口。

---

*Originally published on [zxzyy10](https://paragraph.com/@zxzyy10/defi-uniswap-amm)*
