// SPDX-License-Identifier: GPLv2
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
// @author Wivern for Beefy.Finance
// @notice This contract adds liquidity to Uniswap V2 compatible liquidity pair pools and stake.
pragma solidity >=0.6.2;
//导入Pair接口
import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';
//导入一个平方根计算工具
import '@uniswap/lib/contracts/libraries/Babylonian.sol';
//导入Router02接口
import 'https://github.com/Uniswap/uniswap-v2-periphery/blob/master/contracts/interfaces/IUniswapV2Router02.sol';
//导入SafeERC20
import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.3.0/contracts/token/ERC20/SafeERC20.sol';
//导入LowGasSafeMath
import 'https://github.com/Uniswap/uniswap-v3-core/blob/main/contracts/libraries/LowGasSafeMath.sol';
//定义一个WETH池的接口
interface IWETH is IERC20 {
function deposit() external payable;
function withdraw(uint256 wad) external;
}
//定义一个Beefu池的接口
interface IBeefyVaultV6 is IERC20 {
function deposit(uint256 amount) external;
function withdraw(uint256 shares) external;
function want() external pure returns (address);
}
contract BeefyUniV2Zap {
using LowGasSafeMath for uint256;
using SafeERC20 for IERC20;
using SafeERC20 for IBeefyVaultV6;
//提供一个不可变的router
IUniswapV2Router02 public immutable router;
//提供一个不可变的WETH池地址
address public immutable WETH;
//提供一个不可变的最小交易量
uint256 public constant minimumAmount = 1000;
//部署合约时需提供一个不可更改的router地址、一个不可更改的WETH池地址
constructor(address _router, address _WETH) {
router = IUniswapV2Router02(_router);
WETH = _WETH;
}
//当外界与本合约进行交互,但并未调用函数或传递数据时,receive()被触发,
//使得本合约可以接受外界转账,但必须保证转账人==WETH池,
//即不接收非WETH池以外的对象的莫名其妙的转账
receive() external payable {
assert(msg.sender == WETH);
}
//用户调用该函数,注入ETH,提交其要访问的beefy池地址、最终兑换到的流动性代币的最小量(后台自动为用户提供)
function beefInETH (address beefyVault, uint256 tokenAmountOutMin) external payable {
//保证用户注入的ETH>=最小交易量
require(msg.value >= minimumAmount, 'Beefy: Insignificant input amount');
//将ETH注入WETH池,兑换WETH
IWETH(WETH).deposit{value: msg.value}();
//提交用户想要访问beefy池地址、最终兑换到的流动性代币的最小量、用户注入的币种(在这里自动兑换成了WETH)
_swapAndStake(beefyVault, tokenAmountOutMin, WETH);
}
//功能同上,用户注入的代币为任何REC20代币
function beefIn (address beefyVault, uint256 tokenAmountOutMin, address tokenIn, uint256 tokenInAmount) external {
require(tokenInAmount >= minimumAmount, 'Beefy: Insignificant input amount');
//allowance()得到A地址授权给B地址的token使用量
//保证用户授权给本合约的tokenIn使用量>=用户想要注入的量
require(IERC20(tokenIn).allowance(msg.sender, address(this)) >= tokenInAmount, 'Beefy: Input token is not approved');
//把tokenIn从用户手里转移到本合约中
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), tokenInAmount);
//进行swap和注入
_swapAndStake(beefyVault, tokenAmountOutMin, tokenIn);
}
//用户提取资产,提交想要互动的beefy池地址、想要撤出的资产量
function beefOut (address beefyVault, uint256 withdrawAmount) external {
//获取beefy池对象、pair
(IBeefyVaultV6 vault, IUniswapV2Pair pair) = _getVaultPair(beefyVault);
//把用户的beefy池代币转入本合约
IERC20(beefyVault).safeTransferFrom(msg.sender, address(this), withdrawAmount);
//向beefy池申请撤资(提交beefy池代币,换回原本资产,即流动性代币)
vault.withdraw(withdrawAmount);
//如果交易对中没有WETH,则直接提取流动性,接收地址直接设为用户地址
if (pair.token0() != WETH && pair.token1() != WETH) {
return _removeLiqudity(address(pair), msg.sender);
}
//若交易对中有ETH,则需要将资产传回本地后,通过_returnAssets把资产传回给用户,
//因为其有处理WETH和ETH兑换的步骤
_removeLiqudity(address(pair), address(this));
address[] memory tokens = new address[](2);
tokens[0] = pair.token0();
tokens[1] = pair.token1();
_returnAssets(tokens);
}
//在用户提取资产的基础上,增加了一个把两种代币换回成一种的操作
//提及用户想要互动的beefy池、撤资额、想要撤资的token、想要撤资的token交易最小单位
function beefOutAndSwap(address beefyVault, uint256 withdrawAmount, address desiredToken, uint256 desiredTokenOutMin) external {
//获取beefy池对象,获取beefy池的pair交易对
(IBeefyVaultV6 vault, IUniswapV2Pair pair) = _getVaultPair(beefyVault);
//获取代币地址
address token0 = pair.token0();
address token1 = pair.token1();
//保证交易对中的代币有用户想要兑换的币种
require(token0 == desiredToken || token1 == desiredToken, 'Beefy: desired token not present in liqudity pair');
//把用户的vault代币转进本合约
vault.safeTransferFrom(msg.sender, address(this), withdrawAmount);
//撤资,取回流动性代币
vault.withdraw(withdrawAmount);
//兑换流动性代币,接收到的流动性存在本合约
_removeLiqudity(address(pair), address(this));
//获取想要兑换掉的代币地址
address swapToken = token1 == desiredToken ? token0 : token1;
address[] memory path = new address[](2);
//兑换链上第一个为想要兑换掉的代币,第二个为用户想要得到的
path[0] = swapToken;
path[1] = desiredToken;
//授权router可以调用本合约的swapToken(将被换掉的token)
_approveTokenIfNeeded(path[0], address(router));
//在router提供的池子里兑换掉
router.swapExactTokensForTokens(IERC20(swapToken).balanceOf(address(this)), desiredTokenOutMin, path, address(this), block.timestamp);
//把资金传给用户
_returnAssets(path);
}
//提交交易对、收币者地址,提取流动性
function _removeLiqudity(address pair, address to) private {
//把本合约持有的pair币(流动性代币)余额的全部量转给pair池子
IERC20(pair).safeTransfer(pair, IERC20(pair).balanceOf(address(this)));
//调用Pair.burn(to),把pair池子中收到的流动性代币烧掉,同时给to发放兑换出的流动性
(uint256 amount0, uint256 amount1) = IUniswapV2Pair(pair).burn(to);
//保证兑换出的流动性量>最小交易额,此时验证是否晚了?待考虑
require(amount0 >= minimumAmount, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
require(amount1 >= minimumAmount, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
}
//提交一个beefy交易池地址,获取beefy池对象,及一个交易对
function _getVaultPair (address beefyVault) private view returns (IBeefyVaultV6 vault, IUniswapV2Pair pair) {
//将beefy交易池地址转化为VaultV6类型,即为beefy池对象
vault = IBeefyVaultV6(beefyVault);
//通过want()获取到beefy交易池对象的交易对
pair = IUniswapV2Pair(vault.want());
//保证pair的工厂==router的工厂,
//即确保beefy池子提供的交易对服务与其调用的router的交易对对应的上
require(pair.factory() == router.factory(), 'Beefy: Incompatible liquidity pair factory');
}
//兑换代币并注入router提供的de-fi池
function _swapAndStake(address beefyVault, uint256 tokenAmountOutMin, address tokenIn) private {
//给_getVaultPair()提供一个beefy池地址,获取到一个beefy池对象、一个交易对
(IBeefyVaultV6 vault, IUniswapV2Pair pair) = _getVaultPair(beefyVault);
//调用UniswapV2Pair的getReserves(),获得交易对中两种代币的储备量
(uint256 reserveA, uint256 reserveB,) = pair.getReserves();
//保证两种代币的储备量>最小交易量
require(reserveA > minimumAmount && reserveB > minimumAmount, 'Beefy: Liquidity pair reserves too low');
//若交易对中的第一个代币类型==调用函数时用户提交的想要注入的代币类型,则将isInputA标记为true
bool isInputA = pair.token0() == tokenIn;
//保证isInputA为真,或者交易对中的第二个代币类型==调用函数时用户提交的想要注入的代币类型,
//即保证了tokenIn要么为token0,要么为token1
require(isInputA || pair.token1() == tokenIn, 'Beefy: Input token not present in liqudity pair');
//创建一个用于存放交易链的数列
address[] memory path = new address[](2);
//交易链中的第一个坑放入用户注入的代币
path[0] = tokenIn;
//交易链中的第二个坑放入另外一种代币
path[1] = isInputA ? pair.token1() : pair.token0();
//该参数存放用户的总投资量,
uint256 fullInvestment = IERC20(tokenIn).balanceOf(address(this));
uint256 swapAmountIn;
//若token0为用户注入的资产,则调用函数时提交参数的顺序为reserveA在前
//通过_getSwapAmount()函数,提交注入单一币数量,两种代币储备量,得到应该用多少单一币先换取另一种币
if (isInputA) {
swapAmountIn = _getSwapAmount(fullInvestment, reserveA, reserveB);
} else {
swapAmountIn = _getSwapAmount(fullInvestment, reserveB, reserveA);
}
//授权router可以调用本合约的token0,此时本合约的池子中只有用户提交的token0
_approveTokenIfNeeded(path[0], address(router));
//定义一个交换量数列
//swap函数的功能为,计算以一种明确的token能换取另一种token的量,并执行该交易,
//可以链式交易,所以返回值为一个数列
//提交的参数为注入token0的数量、取出token1的最小量、交易链、收币地址、交易最晚时间
uint256[] memory swapedAmounts = router
.swapExactTokensForTokens(swapAmountIn, tokenAmountOutMin, path, address(this), block.timestamp);
//授权router可以调用本合约的token1,此时本合约的池子中已有兑换来的token1
_approveTokenIfNeeded(path[1], address(router));
//addLiquidity的功能为提交token0、1地址,提交A、B期望投入量,提交A、B最小交易量,提交收币地址、deadline
//返回值为交易的A、B量,及兑换到的流动性代币数量
//amountLiquidity记录的是成交后,用户得到的流动性代币数量,这里的用户为本合约(address(this))
(,, uint256 amountLiquidity) = router
.addLiquidity(path[0], path[1], fullInvestment.sub(swapedAmounts[0]), swapedAmounts[1], 1, 1, address(this), block.timestamp);
//授权vault(beefy池对象)可以调用交易对
_approveTokenIfNeeded(address(pair), address(vault));
//beefy池中注入流动性代币数量的beefy池中定义的币
vault.deposit(amountLiquidity);
//把在vault池中的vault币转给用户,转账量为本合约在vault池中的全部余额
vault.safeTransfer(msg.sender, vault.balanceOf(address(this)));
//把余出来的token0、1退还用户
_returnAssets(path);
}
//提交一个token数列,
function _returnAssets(address[] memory tokens) private {
uint256 balance;
//正序依次读取tokens[]
for (uint256 i; i < tokens.length; i++) {
//读取本合约的tokens[i]币的余额
balance = IERC20(tokens[i]).balanceOf(address(this));
//如果有余额
if (balance > 0) {
//如果该币为WETH
if (tokens[i] == WETH) {
//从WETH池中取出全部余额
IWETH(WETH).withdraw(balance);
//把全部ETH转给用户
(bool success,) = msg.sender.call{value: balance}(new bytes(0));
//保证转账成功
require(success, 'Beefy: ETH transfer failed');
} else {
//如果该币不为WETH,直接转给用户
IERC20(tokens[i]).safeTransfer(msg.sender, balance);
}
}
}
}
//提交投入A的量、AB在池子中的储备量,得到换得的量
function _getSwapAmount(uint256 investmentA, uint256 reserveA, uint256 reserveB) private view returns (uint256 swapAmount) {
uint256 halfInvestment = investmentA / 2;
//getAmountOut()函数为V2Router借用的V2Library中的函数,
//其功能为提交Atoken注入量、A储备量、B储备量,得到能兑换出的Btoken量
//中间参数 = halfInvestment能兑换出的B的最大量
uint256 nominator = router.getAmountOut(halfInvestment, reserveA, reserveB);
//quote()函数同样为V2Router借用的V2Library中的函数
//其功能为提交A期望的交易量、A储备量、B储备量,得到与A等价的B的数量
//分母 = halfInvestment数量的tokenA,在提交到资产量为reserveA + halfInvestment,reserveB - nominator的
//交易对池子之后,能够换回多少等量的tokenB
uint256 denominator = router.quote(halfInvestment, reserveA.add(halfInvestment), reserveB.sub(nominator));
//应该拿多少A换B = tokenA的投入量 - 开方(halfInvestment * halfInvestment * 一半A能兑换出的B最大量 / 在调整后的池子中一半A等于多少B)
//计算细节待研究
swapAmount = investmentA.sub(Babylonian.sqrt(halfInvestment * halfInvestment * nominator / denominator));
}
//对交换进行估计
//提交想要互动的beefy池、注入的token地址、全部投资额,
//返回注入的交换量、得到的交换量、得到的token地址
function estimateSwap(address beefyVault, address tokenIn, uint256 fullInvestmentIn) public view returns(uint256 swapAmountIn, uint256 swapAmountOut, address swapTokenOut) {
//检查WETH池
checkWETH();
//获取交易对
(, IUniswapV2Pair pair) = _getVaultPair(beefyVault);
bool isInputA = pair.token0() == tokenIn;
require(isInputA || pair.token1() == tokenIn, 'Beefy: Input token not present in liqudity pair');
(uint256 reserveA, uint256 reserveB,) = pair.getReserves();
(reserveA, reserveB) = isInputA ? (reserveA, reserveB) : (reserveB, reserveA);
swapAmountIn = _getSwapAmount(fullInvestmentIn, reserveA, reserveB);
swapAmountOut = router.getAmountOut(swapAmountIn, reserveA, reserveB);
swapTokenOut = isInputA ? pair.token1() : pair.token0();
}
//检查WETH兑换池是否有效
function checkWETH() public view returns (bool isValid) {
//若WETH池为router提供的WETH,则isValid为有效(true)
isValid = WETH == router.WETH();
//保证isValid为有效
require(isValid, 'Beefy: WETH address not matching Router.WETH()');
}
//授权token,如果需要的话
//提交token地址、调用token的用户地址
function _approveTokenIfNeeded(address token, address spender) private {
//如果本合约未授权spender调用token
if (IERC20(token).allowance(address(this), spender) == 0) {
//则授权spender使用本合约的token
IERC20(token).safeApprove(spender, uint256(~0));
}
}
}