记得朋友圈看到过一句话,如果Defi是以太坊的皇冠,那么Uniswap就是这顶皇冠中的明珠。Uniswap目前已经是V2版本,相对V1,它的功能更加全面优化,然而其合约源码却并不复杂。本文为个人学习UniswapV2源码的系列记录文章。
一、ExampleFlashSwap合约介绍
该合约为利用UniswapV2交易对中的FlashSwap的先借后还特性,在买卖资产的同时和UnisapV1交易对进行交易,利用价格差进行套利。
二、ExampleFlashSwap合约源码
pragma solidity =0.6.6;
import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Callee.sol';
import '../libraries/UniswapV2Library.sol';
import '../interfaces/V1/IUniswapV1Factory.sol';
import '../interfaces/V1/IUniswapV1Exchange.sol';
import '../interfaces/IUniswapV2Router01.sol';
import '../interfaces/IERC20.sol';
import '../interfaces/IWETH.sol';
contract ExampleFlashSwap is IUniswapV2Callee {
IUniswapV1Factory immutable factoryV1;
address immutable factory;
IWETH immutable WETH;
constructor(address _factory, address _factoryV1, address router) public {
factoryV1 = IUniswapV1Factory(_factoryV1);
factory = _factory;
WETH = IWETH(IUniswapV2Router01(router).WETH());
}
// needs to accept ETH from any V1 exchange and WETH. ideally this could be enforced, as in the router,
// but it's not possible because it requires a call to the v1 factory, which takes too much gas
receive() external payable {
}
// gets tokens/WETH via a V2 flash swap, swaps for the ETH/tokens on V1, repays V2, and keeps the rest!
function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external override {
address[] memory path = new address[](2);
uint amountToken;
uint amountETH;
{
// scope for token{0,1}, avoids stack too deep errors
address token0 = IUniswapV2Pair(msg.sender).token0();
address token1 = IUniswapV2Pair(msg.sender).token1();
assert(msg.sender == UniswapV2Library.pairFor(factory, token0, token1)); // ensure that msg.sender is actually a V2 pair
assert(amount0 == 0 || amount1 == 0); // this strategy is unidirectional
path[0] = amount0 == 0 ? token0 : token1;
path[1] = amount0 == 0 ? token1 : token0;
amountToken = token0 == address(WETH) ? amount1 : amount0;
amountETH = token0 == address(WETH) ? amount0 : amount1;
}
assert(path[0] == address(WETH) || path[1] == address(WETH)); // this strategy only works with a V2 WETH pair
IERC20 token = IERC20(path[0] == address(WETH) ? path[1] : path[0]);
IUniswapV1Exchange exchangeV1 = IUniswapV1Exchange(factoryV1.getExchange(address(token))); // get V1 exchange
if (amountToken > 0) {
(uint minETH) = abi.decode(data, (uint)); // slippage parameter for V1, passed in by caller
token.approve(address(exchangeV1), amountToken);
uint amountReceived = exchangeV1.tokenToEthSwapInput(amountToken, minETH, uint(-1));
uint amountRequired = UniswapV2Library.getAmountsIn(factory, amountToken, path)[0];
assert(amountReceived > amountRequired); // fail if we didn't get enough ETH back to repay our flash loan
WETH.deposit{
value: amountRequired}();
assert(WETH.transfer(msg.sender, amountRequired)); // return WETH to V2 pair
(bool success,) = sender.call{
value: amountReceived - amountRequired}(new bytes(0)); // keep the rest! (ETH)
assert(success);
} else {
(uint minTokens) = abi.decode(data, (uint)); // slippage parameter for V1, passed in by caller
WETH.withdraw(amountETH);
uint amountReceived = exchangeV1.ethToTokenSwapInput{
value: amountETH}(minTokens, uint(-1));
uint amountRequired = UniswapV2Library.getAmountsIn(factory, amountETH, path)[0];
assert(amountReceived > amountRequired); // fail if we didn't get enough tokens back to repay our flash loan
assert(token.transfer(msg.sender, amountRequired)); // return tokens to V2 pair
assert(token.transfer(sender, amountReceived - amountRequired)); // keep the rest! (tokens)
}
}
}
三、源码其它部分学习
-
第一行,照例是指定Solidity版本
-
第二行,导入
IUniswapV2Callee
接口,该接口定义了一个接收到代币后的回调函数。在Uniswapv2核心合约中的交易对合约的swap
函数有这么一行代码:if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
。这正是调用了该函数。这行代码在用户得到买入的资产后立即调用了,且发生在用户卖出资产之前。用户可以在这个空隙利用uniswapV2Call
这个回调函数做自己想做的任意操作,比如说套利等。因此,此回调函数再加上UniswapV2交易对的先买进再卖出机制是实现套利的核心。 -
接下来六个
import
函数分别导入UniswapV1版本的factory
合约接口和交易对接口,V2版的工具库及Router
接口,标准ERC20代币接口和WETH接口。因为V1版本的交易对为ETH/ERC20交易对,所以V2版本的交易对相应为WETH/ERC20交易对,所以需要用到WETH及ERC20接口。 -
contract ExampleFlashSwap is IUniswapV2Callee {
这一行为合约定义,它必须实现IUniswapV2Callee
,也就是必须实现uniswapV2Call
这个函数,不然无法进行回调会报错重置交易。 -
IUniswapV1Factory immutable factoryV1; address immutable factory; IWETH immutable WETH;
接下来是三个状态变量,分别为V1版本的
factory
实例,V2版本的factory
地址及WETH的实例。为什么这里V2版本的factory
为地址类型而不为实例(合约类型)呢?因为下面的IUniswapV2Callee
函数会利用该地址进行大量的计算(见工具库),所以这里使用地址类型更方便一些。 -
接下来是
constructor
构造器,利用输入参数对上面三个状态变量初始化。注意,WETH实例的初始化不是直接传入的WETH合约地址,而是利用Router
合约得到的。其实WETH
合约人人都可以部署一个,是可以存在多个的。如果存在这种情况,到底用哪个地址实例化呢?用Router
合约用到的那个地址才是一致的,是准确无误的。 -
receive() external payable {}
这行代码代表可以接收直接发送的ETH,注释的意思和上一篇文章学习中对应的注释类似,这里不再重复了。
四、uniswapV2Call
函数学习
uniswapV2Call
函数,它的注释清晰的解释了套利的过程。这期间你不需要拥有任何一种交易对中的资产(仅需要有少量的ETH来支付gas费用),俗称空手套白狼。它的四个输入参数为调用者(其实就是最初发起交易的账号)、从V2交易对发送过来的两种资产数量、用户预先定义的数据。
注意上面提到的V2版本交易对的这行代码:
if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
。这就意味着本合约实际上为上述代码中的to
,也就是说用户调用交易对合约的swap
函数时,输入的接收地址to
必须为本合约地址。
下面我们来详细学习该函数:
- 第一行定义了一个
path
来保存两种资产的地址。path
是路径的意思,也可以代表交易路径(流程)。 - 第2-3行定义了两个临时变量来记录买进的ETH/TOKEN数量,方便辨识,不然
token0
与token1
你都不知道是什么。 - 4、13行用来防止
stack too deep errors
,已经讲过多次,不再重复。 - 5-6行用来获取V2交易对的两种代币地址。这里
msg.sender
就是V2的交易对,因为本函数是从V2交易对调用的。见上面列出的那行调用代码。 - 第7行用来验证调用者(交易对)地址和使用UniswapV2工具库算出来的地址相同,确保调用者就是V2交易对,不是假的或者伪装的。
- 第8行注释说的很清楚,单向的。也就是只能卖出一种资产,得到另一种资产。不能两种资产都卖,虽然UniswapV2交易对支持这种操作。但是这里套利是不支持这种操作的,所以只能得到一种资产,其中必定有一种资产为0。那有没有两种资产都为0呢?没有这种可能,在UniswapV2交易对中对买进的资产做了限制,至少要买进一种(大于0)。
- 第9行和第10行就很明显了,设置
path[0]
为卖出的资产(买进的为0),path[1]
为买进的资产,也就是交易路径为path[0]
=>path[1]
。这里需要说明,UniswapV2交易对调用此函数时提供的输入参数amount0
及amount1
是和token0
及token1
对应的,也就是token0
的买进数量为amount0
。 - 第11行用来设置
amountToken
数值。如果token0
为WETH地址,那么另一种资产必为TOKEN,所以其数值为amount1
;否则就是本资产token0
,对应的数值为amount0
。 - 第12行用来设置
amountETH
数值。逻辑同上。 - 第14行进一步验证PATH中必须有一种地址为WETH地址,当然你也可以验证
token0
或者token1
必须有一个为WETH地址,它们是等效的。注释讲了用来确保它是V2中的包含WETH的交易对(否则无法和V1交易对套利),前面第7行只验证了必须为V2交易对。 - 第15行用来获取同时涉及到两种版本交易对的ERC20代币实例。
- 第16行用来获取V1版本相应交易对的实例,它调用了V1版本的
factory
中的getExchange
接口来获取包含该ERC20代币(15行那个实例)的交易对地址。 - 接下来是一个
if - else
语句来根据从UniswapV2交易对得到的是普通ERC20代币还是ETH分情况和UniswapV1的交易对进行交易,最后将得到的另一种资产支付给UniswapV2交易对,自己留下剩余的,实现套利的目的。 - 如果是
amountToken > 0
,那就是从UniswapV2交易对得到了普通ERC20代币,则接着进行:- 第18行将随交易发送的数据
data
解码成uint
格式,并设置成为minETH
的值。这个minETH
是在V1交易对交易时指定得到的ETH最小值。这个解码的语法abi.decode
这里已经是第二次使用了。第一次使用在核心合约中的交易对合约的_safeTransfer
函数中:abi.decode(data, (bool))
,大家可以自己对照看一下。 - 第19行对V1版本的交易对进行获得的代币的授权,因为V1版本交易对是授权交易,不是先转移资产再交易,所以必须授权。
- 第20行调用V1版本交易对相应的函数将TOKEN交易成ETH,也就是卖出TOKEN,得到ETH。参数分别是卖出的TOKEN数量,指定获取的ETH最小数量及最晚交易时间。
- 第21行根据UniswapV2的工具库计算需要支付给UniswapV2交易对的另一种资产WETH的数值。注意:
getAmountsIn
函数返回的是一个数组,它的第一个元素就是卖出的初始资产的数量。具体分析可以参考序列文章中的周边合约学习中的Router
合约学习。 - 第22行验证从V1交易对换回的ETH数量必须大于欲支付给V2交易对的WETH数量,否则不够支付,交易会重置。这里可以看到验证时用了
assert
函数,但是我们有时也会在合约中看到使用require
函数验证。那么什么时候用require
什么时候用assert
呢?一般的原则为:当验证直接外界输入时,使用require
;当验证内部结果时,使用assert
。可以看到这里是验证中间的一个计算结果,所以使用了assert
。 - 第23行,将ETH兑换成等额的欲支付数量的WETH。从第22行知道,这里ETH没有兑换完,还有剩余的,这就是盈利。
- 第24行,将欲支付数量的WETH转移到V2交易对(
msg.sender
),这里就是先借后还的“还”。那什么时候开始借的呢,从调用本函数之前就借给本合约(转移资产到本合约)了。 - 第25-26行,将剩余的ETH发送给调用者(也就是初始用户),并验证发送是否成功。这里使用了一个低级函数
call
,它如果执行失败,并不会重置整个交易,而是返回一个false
,所以这里必须验证返回值。这里为什么不使用更高级的address
类型的transfer
或send
成员呢。个人猜想原因有:- 不易和
WETH.transfer
这种调用语句相区分,可能引起阅读上的混淆; transfer
或send
必须在address payable
类型上使用,需要使用payable(sender)
来转换。- 因为
transfer
或send
函数限定了随函数传输的gas
为2300。万一接收地址是一个合约,它还需要接收ETH后再做别的事,这时便会引起out of gas
,导致交易失败。使用call
可以将所有能得到的gas都传输过去,利于接收方再执行其它操作。 - 小提示,不管是用
transfer
或send
还是call
来发送ETH,接收地址如果是合约的话,必须有相应接收ETH的回调函数,例如本合约中出现的receive
,否则交易会失败。
- 不易和
- 第18行将随交易发送的数据
- 如果是
else
,那就是amountETH > 0
,也就是从UniswapV2交易对得到的是WETH,需要使用它从V1交易对中兑换出来TOKEN,然后再支付TOKEN给V2版本的交易对。- 第28行,解码获得用户输入的最小token数量。
- 第29行,将所有WETH兑换成等额ETH,以便接下来和V1交易对交易。
- 第30行,将所有ETH在V1交易对中交易成TOKEN。
- 第31行,利用UniswapV2的工具库计算需要支付给UniswapV2交易对的TOKEN的数量,它和30行得到的数量差就是盈利的数量。
- 第32行,验证从V1版本换回的TOKEN数量必须大于支付给UniswapV2交易对的TOKEN数量,否则不够支付(盈利为负),会重置交易。
- 第33行,将支付的TOKEN发送到V2交易对,也就是
msg.sender
,这里就是先借后还中的“还”。这里因为使用了assert
函数,所以要求token.transfer
必须返回一个true
。所以这个TOKEN对应的代币合约必需满足这个条件(个人猜想因为代币合约是外部合约,是未知的,有可能不返回值或者返回为false
,所以必须加一个条件)。 - 第34行,将剩余的TOKEN发送给最初用户(
sender
),这里不用考虑接收方(sender
)是合约还是外部账号,因为不是发送ETH。使用assert
同上。
五、其它
大家从这个合约可以看出,套利合约使用没有门槛,但它并不意味着我们随时都可以使用这个套利合约来套利。个人觉得使用条件及限制有:
- 首先套利的两个交易对能资产要一致,这是很明显的,你不能tokenA最后套成了tokenB。
- 其次两个交易对的价格有差别,要有利可套,否则交易回来的资产不够支付的,交易会重置,白白损失手续费。
- 套利到底能套多少未知,无法提前线下计算。因为它和交易时两种交易对交易执行时价格有关,有可能你执行前是可以套利的,但执行时价格回落 ,你就无法套利了。
实际中也有其它DeFi交易对和UniswapV2交易对之间套利的应用,例如DODO这个项目就有一个套利合约UniswapArbitrageur.sol
(不过是针对特定交易对的)。大家有兴趣的可以自己去看一下。
好了,今天的套利合约示例学习就到此结束了,下一次计划学习ExampleOracleSimple.sol
(价格预言机示例合约)。
由于个人能力有限,难免有理解错误或者不正确的地方,还请大家多多留言指正。
来源:oschina
链接:https://my.oschina.net/u/4280362/blog/4691077