diff --git a/contracts/interfaces/liquidation/thirdParty/IDMMExchangeRouter.sol b/contracts/interfaces/liquidation/thirdParty/IDMMExchangeRouter.sol new file mode 100644 index 00000000..58613346 --- /dev/null +++ b/contracts/interfaces/liquidation/thirdParty/IDMMExchangeRouter.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.7.6; + +import {IERC20Ext} from '@kyber.network/utils-sc/contracts/IERC20Ext.sol'; + +/// @dev an simple interface for integration dApp to swap +interface IDMMExchangeRouter { + function swapExactTokensForTokens( + uint256 amountIn, + uint256 amountOutMin, + address[] calldata poolsPath, + IERC20Ext[] calldata path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + + function swapTokensForExactTokens( + uint256 amountOut, + uint256 amountInMax, + address[] calldata poolsPath, + IERC20Ext[] calldata path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + + function swapExactETHForTokens( + uint256 amountOutMin, + address[] calldata poolsPath, + IERC20Ext[] calldata path, + address to, + uint256 deadline + ) external payable returns (uint256[] memory amounts); + + function swapTokensForExactETH( + uint256 amountOut, + uint256 amountInMax, + address[] calldata poolsPath, + IERC20Ext[] calldata path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + + function swapExactTokensForETH( + uint256 amountIn, + uint256 amountOutMin, + address[] calldata poolsPath, + IERC20Ext[] calldata path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + + function swapETHForExactTokens( + uint256 amountOut, + address[] calldata poolsPath, + IERC20Ext[] calldata path, + address to, + uint256 deadline + ) external payable returns (uint256[] memory amounts); + + function getAmountsOut( + uint256 amountIn, + address[] calldata poolsPath, + IERC20Ext[] calldata path + ) external view returns (uint256[] memory amounts); + + function getAmountsIn( + uint256 amountOut, + address[] calldata poolsPath, + IERC20Ext[] calldata path + ) external view returns (uint256[] memory amounts); +} diff --git a/contracts/treasury/liquidateWithKyberDMM/LiquidateFeeWithKyberDMM.sol b/contracts/treasury/liquidateWithKyberDMM/LiquidateFeeWithKyberDMM.sol new file mode 100644 index 00000000..b8fd195b --- /dev/null +++ b/contracts/treasury/liquidateWithKyberDMM/LiquidateFeeWithKyberDMM.sol @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.7.6; + +import {IDMMPool} from '../../interfaces/liquidation/thirdParty/IDMMPool.sol'; +import {IDMMExchangeRouter} from '../../interfaces/liquidation/thirdParty/IDMMExchangeRouter.sol'; +import {PermissionOperators} from '@kyber.network/utils-sc/contracts/PermissionOperators.sol'; +import {Withdrawable} from '@kyber.network/utils-sc/contracts/Withdrawable.sol'; +import {Utils} from '@kyber.network/utils-sc/contracts/Utils.sol'; +import {IERC20Ext} from '@kyber.network/utils-sc/contracts/IERC20Ext.sol'; +import {SafeMath} from '@openzeppelin/contracts/math/SafeMath.sol'; +import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; + +/** + * @dev Contract to help Liquidate Fee from KyberDAO multisig wallet + */ +contract LiquidateFeeWithKyberDMM is PermissionOperators, Withdrawable, Utils { + using SafeMath for uint256; + using SafeERC20 for IERC20Ext; + + /// KyberDMM router + IDMMExchangeRouter public immutable dmmRouter; + /// address to receive token after liquidation + address public immutable recipient; + + mapping (address => mapping (address => IERC20Ext[])) internal tokenPath; + mapping (address => mapping (address => address[])) internal poolPath; + + event LiquidateWithKyberDMM( + address indexed caller, + IERC20Ext[] sources, + uint256[] amounts, + IERC20Ext dest, + uint256 minDestAmount, + uint256 actualDestAmount + ); + + constructor( + address admin, + address _recipient, + IDMMExchangeRouter _router + ) Withdrawable(admin) { + recipient = _recipient; + dmmRouter = _router; + } + + receive() external payable {} + + function manualApproveAllowances(IERC20Ext[] calldata tokens, bool isReset) + external onlyOperator + { + for(uint256 i = 0; i < tokens.length; i++) { + _safeApproveAllowance( + tokens[i], + address(dmmRouter), + isReset ? 0 : type(uint256).max + ); + } + } + + /** + * @dev Set token and pool path from src to dest token + */ + function setTradePath( + address src, + address dest, + IERC20Ext[] calldata _tokenPath, + address[] calldata _poolPath + ) external onlyOperator { + require(_tokenPath.length == _poolPath.length + 1, 'invalid lengths'); + require(src == address(_tokenPath[0]), 'invalid src value'); + require(dest == address(_tokenPath[_tokenPath.length - 1]), 'invalid dest token'); + tokenPath[src][dest] = _tokenPath; + poolPath[src][dest] = _poolPath; + } + + /** + * @dev Anyone can call this function to liquidate LP/normal tokens to a dest token + * To save gas, should specify the list of final tokens to swap to dest token + * Pass list of tradeTokens + corresponding balances before the liquidation happens + * as txData, will be used to get the received amount of each token to swap + * @param source address to collect LP tokens from + * @param lpTokens list of source tokens + * @param amounts amount of each source token + * @param dest dest token to swap to + * @param tradeTokens list of final tokens to swap to dest token after removing liquidities + * @param minReturn minimum amount of destToken to be received + */ + function liquidate( + address source, + IERC20Ext[] calldata lpTokens, + uint256[] calldata amounts, + IERC20Ext dest, + IERC20Ext[] calldata tradeTokens, + uint256 minReturn + ) external onlyOperator { + require(lpTokens.length == amounts.length, 'invalid lengths'); + + uint256 destBalanceBefore = dest.balanceOf(address(this)); + _removeLiquidity(source, lpTokens, amounts); + + uint256 totalReturn = _swapWithKyberDMM(tradeTokens, destBalanceBefore, dest); + + require(totalReturn >= minReturn, 'totalReturn < minReturn'); + dest.safeTransfer(recipient, totalReturn); + + emit LiquidateWithKyberDMM( + tx.origin, + lpTokens, + amounts, + dest, + minReturn, + totalReturn + ); + } + + /** + * @dev Take a list of lpTokens and remove all liquidity + */ + function _removeLiquidity( + address source, + IERC20Ext[] memory lpTokens, + uint256[] memory amounts + ) + internal + { + for(uint256 i = 0; i < lpTokens.length; i++) { + lpTokens[i].safeTransferFrom(source, address(lpTokens[i]), amounts[i]); + IDMMPool(address(lpTokens[i])).burn(address(this)); + } + } + + /** + * @dev Simple swap with KyberDMM + */ + function _swapWithKyberDMM( + IERC20Ext[] memory tradeTokens, + uint256 destTokenBefore, + IERC20Ext dest + ) + internal returns (uint256 totalReturn) + { + for(uint256 i = 0; i < tradeTokens.length; i++) { + if (tradeTokens[i] == dest) continue; + uint256 amount = getBalance(tradeTokens[i], address(this)); + if (amount == 0) continue; + _safeApproveAllowance(tradeTokens[i], address(dmmRouter), type(uint256).max); + uint256[] memory amounts = dmmRouter.swapExactTokensForTokens( + amount, + 1, + poolPath[address(tradeTokens[i])][address(dest)], + tokenPath[address(tradeTokens[i])][address(dest)], + address(this), + block.timestamp + 1000 + ); + require(amounts[amounts.length - 1] > 0, '0 amount out'); + } + totalReturn = getBalance(dest, address(this)).sub(destTokenBefore); + } + + function getTradePath(address src, address dest) + external view + returns (IERC20Ext[] memory _tokenPath, address[] memory _poolPath) { + _tokenPath = tokenPath[src][dest]; + _poolPath = poolPath[src][dest]; + } + + /** + * @dev Estimate amount out from list of lp tokens + * @notice It is just for references, since a pool can be traded multiple times + */ + function estimateReturns( + address[] calldata lpTokens, + uint256[] calldata amountIns, + address dest + ) external view returns (uint256 amountOut) { + require(lpTokens.length == amountIns.length, 'invalid lengths'); + uint256[] memory amountsOut; + for (uint256 i = 0; i < lpTokens.length; i++) { + (IERC20Ext[2] memory tokens, uint256[2] memory amounts) = + _getExpectedTokensFromLp(lpTokens[i], amountIns[i]); + + for (uint256 j = 0; j <= 1; j++) { + if (tokens[j] == IERC20Ext(dest)) { + amountOut += amounts[j]; + continue; + } + amountsOut = dmmRouter.getAmountsOut( + amounts[j], + poolPath[address(tokens[j])][dest], + tokenPath[address(tokens[j])][dest] + ); + amountOut += amountsOut[amountsOut.length - 1]; + } + } + } + + // call approve only if amount is 0 or the current allowance is 0, only for tokens + function _safeApproveAllowance(IERC20Ext token, address spender, uint256 amount) internal { + if (amount == 0 || token.allowance(address(this), spender) == 0) { + token.safeApprove(spender, amount); + } + } + + function _getExpectedTokensFromLp( + address pool, + uint256 lpAmount + ) + public view + returns ( + IERC20Ext[2] memory tokens, + uint256[2] memory amounts + ) + { + uint256 totalSupply = IERC20Ext(pool).totalSupply(); + (tokens[0], tokens[1]) = (IDMMPool(pool).token0(), IDMMPool(pool).token1()); + uint256 amount0; + uint256 amount1; + ( + amount0, + amount1, + , // virtual balance 0 + , // virtual balance 1 + // fee in precision + ) = IDMMPool(pool).getTradeInfo(); + + (amounts[0], amounts[1]) = ( + amount0.mul(lpAmount) / totalSupply, + amount1.mul(lpAmount) / totalSupply + ); + } +} diff --git a/deployment/liquidateFee/bsc_mainnet_input.json b/deployment/liquidateFee/bsc_mainnet_input.json new file mode 100644 index 00000000..e2a364aa --- /dev/null +++ b/deployment/liquidateFee/bsc_mainnet_input.json @@ -0,0 +1,83 @@ +{ + "admin": "0x4BD6037E5CF0cADB0CcE85691A5723BC94Ae2faE", + "recipient": "0x91c9D4373B077eF8082F468C7c97f2c499e36F5b", + "dmmRouter": "0x78df70615ffc8066cc0887917f2Cd72092C86409", + "destToken": "0xfe56d5892bdffc7bf58f2e84be1b2c32d21c308b", + "tokenConfigs": [ + { + "token": "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c", + "poolPath": [ + "0x6170b6d96167346896169b35e1e9585feab873bb" + ], + "tokenPath": [ + "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c", + "0xfe56d5892bdffc7bf58f2e84be1b2c32d21c308b" + ] + }, + { + "token": "0x55d398326f99059ff775485246999027b3197955", + "poolPath": [ + "0xec303ce1edbebf7e71fc7b350341bb6a6a7a6381", + "0x6170b6d96167346896169b35e1e9585feab873bb" + ], + "tokenPath": [ + "0x55d398326f99059ff775485246999027b3197955", + "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c", + "0xfe56d5892bdffc7bf58f2e84be1b2c32d21c308b" + ] + }, + { + "token": "0x2170ed0880ac9a755fd29b2688956bd959f933f8", + "poolPath": [ + "0xd26fa4d47ab61c03259f0cbc9054890df5c3b7ad", + "0x6170b6d96167346896169b35e1e9585feab873bb" + ], + "tokenPath": [ + "0x2170ed0880ac9a755fd29b2688956bd959f933f8", + "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c", + "0xfe56d5892bdffc7bf58f2e84be1b2c32d21c308b" + ] + }, + { + "token": "0x633237c6fa30fae46cc5bb22014da30e50a718cc", + "poolPath": [ + "0xf81e106c5b44ba9a993fc1f456a4c8e54c47cf34", + "0xec303ce1edbebf7e71fc7b350341bb6a6a7a6381", + "0x6170b6d96167346896169b35e1e9585feab873bb" + ], + "tokenPath": [ + "0x633237c6fa30fae46cc5bb22014da30e50a718cc", + "0x55d398326f99059ff775485246999027b3197955", + "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c", + "0xfe56d5892bdffc7bf58f2e84be1b2c32d21c308b" + ] + }, + { + "token": "0xe8176d414560cfe1bf82fd73b986823b89e4f545", + "poolPath": [ + "0x2d49f16c9ad4f1145bb27c9af71474f468a697c8", + "0x6170b6d96167346896169b35e1e9585feab873bb" + ], + "tokenPath": [ + "0xe8176d414560cfe1bf82fd73b986823b89e4f545", + "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c", + "0xfe56d5892bdffc7bf58f2e84be1b2c32d21c308b" + ] + } + ], + "liquidateTokens": [ + "0xec303ce1edbebf7e71fc7b350341bb6a6a7a6381", + "0x6170b6d96167346896169b35e1e9585feab873bb", + "0xd26fa4d47ab61c03259f0cbc9054890df5c3b7ad", + "0xf81e106c5b44ba9a993fc1f456a4c8e54c47cf34", + "0x2d49f16c9ad4f1145bb27c9af71474f468a697c8" + ], + "tradeTokens": [ + "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c", + "0x55d398326f99059ff775485246999027b3197955", + "0x2170ed0880ac9a755fd29b2688956bd959f933f8", + "0x633237c6fa30fae46cc5bb22014da30e50a718cc", + "0xe8176d414560cfe1bf82fd73b986823b89e4f545" + ], + "outputFilename": "bsc_mainnet_output.json" +} diff --git a/deployment/liquidateFee/bsc_testnet_input.json b/deployment/liquidateFee/bsc_testnet_input.json new file mode 100644 index 00000000..fd319c77 --- /dev/null +++ b/deployment/liquidateFee/bsc_testnet_input.json @@ -0,0 +1,40 @@ +{ + "contractAddress": "0xf8d3a92b470c339457cb2b70a05d27c3a8983e40", + "admin": "0x4BD6037E5CF0cADB0CcE85691A5723BC94Ae2faE", + "recipient": "0x4BD6037E5CF0cADB0CcE85691A5723BC94Ae2faE", + "dmmRouter": "0x19395624c030a11f58e820c3aefb1f5960d9742a", + "destToken": "0x51e8d106c646ca58caf32a47812e95887c071a62", + "tokenConfigs": [ + { + "token": "0xae13d989dac2f0debff460ac112a837c89baa7cd", + "poolPath": [ + "0x209d9e4cd05ff6f6aa6abe65c92ef76df36001a6" + ], + "tokenPath": [ + "0xae13d989dac2f0debff460ac112a837c89baa7cd", + "0x51e8d106c646ca58caf32a47812e95887c071a62" + ] + }, + { + "token": "0xbb843a2296f9aa49070eb2dcd482f23548238f65", + "poolPath": [ + "0xfe1687d57e48f5152cbbcef9fc590ef045ee96c2", + "0x209d9e4cd05ff6f6aa6abe65c92ef76df36001a6" + ], + "tokenPath": [ + "0xbb843a2296f9aa49070eb2dcd482f23548238f65", + "0xae13d989dac2f0debff460ac112a837c89baa7cd", + "0x51e8d106c646ca58caf32a47812e95887c071a62" + ] + } + ], + "liquidateTokens": [ + "0xfe1687d57e48f5152cbbcef9fc590ef045ee96c2", + "0x209d9e4cd05ff6f6aa6abe65c92ef76df36001a6" + ], + "tradeTokens": [ + "0xbb843a2296f9aa49070eb2dcd482f23548238f65", + "0xae13d989dac2f0debff460ac112a837c89baa7cd" + ], + "outputFilename": "bsc_testnet_output.json" +} diff --git a/deployment/liquidateFee/bsc_testnet_output.json b/deployment/liquidateFee/bsc_testnet_output.json new file mode 100644 index 00000000..06c78b94 --- /dev/null +++ b/deployment/liquidateFee/bsc_testnet_output.json @@ -0,0 +1,31 @@ +{ + "contractAddress": "0xf8d3a92b470c339457cb2b70a05d27c3a8983e40", + "tokenConfigs": [ + { + "token": "0xae13d989dac2f0debff460ac112a837c89baa7cd", + "poolPath": [ + "0x209d9e4cd05ff6f6aa6abe65c92ef76df36001a6" + ], + "tokenPath": [ + "0xae13d989dac2f0debff460ac112a837c89baa7cd", + "0x51e8d106c646ca58caf32a47812e95887c071a62" + ] + }, + { + "token": "0xbb843a2296f9aa49070eb2dcd482f23548238f65", + "poolPath": [ + "0xfe1687d57e48f5152cbbcef9fc590ef045ee96c2", + "0x209d9e4cd05ff6f6aa6abe65c92ef76df36001a6" + ], + "tokenPath": [ + "0xbb843a2296f9aa49070eb2dcd482f23548238f65", + "0xae13d989dac2f0debff460ac112a837c89baa7cd", + "0x51e8d106c646ca58caf32a47812e95887c071a62" + ] + } + ], + "liquidateTokens": [ + "0xfe1687d57e48f5152cbbcef9fc590ef045ee96c2", + "0x209d9e4cd05ff6f6aa6abe65c92ef76df36001a6" + ] +} \ No newline at end of file diff --git a/deployment/liquidateFee/deployLiquidateFeeWithKyberDMM.js b/deployment/liquidateFee/deployLiquidateFeeWithKyberDMM.js new file mode 100644 index 00000000..920506cf --- /dev/null +++ b/deployment/liquidateFee/deployLiquidateFeeWithKyberDMM.js @@ -0,0 +1,142 @@ +require('@nomiclabs/hardhat-ethers'); +const fs = require('fs'); +const path = require('path'); + + +let gasPrice; + +async function verifyContract(hre, contractAddress, ctorArgs) { + await hre.run('verify:verify', { + address: contractAddress, + constructorArguments: ctorArgs, + }); +} + +let deployerAddress; +let contractAddress; +let admin; +let recipient; +let dmmRouter; +let tokenConfigs; +let destToken; +let liquidateTokens; +let tradeTokens; +let outputFilename; + +function verifyArrays(arr1, arr2) { + if (arr1.length != arr2.length) return false; + for (let i = 0; i < arr1.length; i++) { + if (arr1[i].toLowerCase() != arr2[i].toLowerCase()) return false; + } + return true; +} + +task('deployLiquidateFeeWithKyberDMM', 'deploy liquidity mining contracts') + .addParam('gasprice', 'The gas price (in gwei) for all transactions') + .addParam('input', 'Input file') + .setAction(async (taskArgs, hre) => { + const configPath = path.join(__dirname, `./${taskArgs.input}`); + const configParams = JSON.parse(fs.readFileSync(configPath, 'utf8')); + parseInput(configParams); + + const BN = ethers.BigNumber; + const [deployer] = await ethers.getSigners(); + deployerAddress = await deployer.getAddress(); + console.log(`Deployer address: ${deployerAddress}`) + + let outputData = {}; + gasPrice = new BN.from(10**9 * taskArgs.gasprice); + console.log(`Deploy gas price: ${gasPrice.toString(10)} (${taskArgs.gasprice} gweis)`); + + const MockToken = await ethers.getContractFactory('KyberNetworkTokenV2'); + + const LiquidateFeeWithKyberDMM = await ethers.getContractFactory('LiquidateFeeWithKyberDMM'); + let liquidateFee; + if (contractAddress == undefined) { + liquidateFee = await LiquidateFeeWithKyberDMM.deploy(admin, recipient, dmmRouter, { gasPrice: gasPrice }); + await liquidateFee.deployed(); + contractAddress = liquidateFee.address; + await liquidateFee.addOperator(deployerAddress, { gasPrice: gasPrice }); + } else { + liquidateFee = await LiquidateFeeWithKyberDMM.attach(contractAddress); + } + console.log(`LiquidateFeeWithKyberDMM address: ${liquidateFee.address}`); + outputData["contractAddress"] = liquidateFee.address; + + outputData["tokenConfigs"] = tokenConfigs; + for (let i = 0; i < tokenConfigs.length; i++) { + let data = await liquidateFee.getTradePath(tokenConfigs[i].token, destToken); + if (verifyArrays(data._tokenPath, tokenConfigs[i].tokenPath) && verifyArrays(data._poolPath, tokenConfigs[i].poolPath)) { + continue; + } + console.log(`Set trade path for token: ${tokenConfigs[i].token}`); + console.log(` token path: ${tokenConfigs[i].tokenPath}`); + console.log(` pool path: ${tokenConfigs[i].poolPath}`); + await liquidateFee.setTradePath( + tokenConfigs[i].token, destToken, tokenConfigs[i].tokenPath, tokenConfigs[i].poolPath + ); + } + + outputData["liquidateTokens"] = liquidateTokens; + let shouldLiquidate = true; + let amountsIn = []; + let pools = []; + if (liquidateTokens != undefined && liquidateTokens.length > 0) { + for (let i = 0; i < liquidateTokens.length; i++) { + let token = await MockToken.attach(liquidateTokens[i]); + let allowance = await token.allowance(recipient, liquidateFee.address); + let balance = await token.balanceOf(recipient); + if (allowance.gt(balance) && balance.gt(BN.from(1))) { + amountsIn.push(balance.sub(1)); + pools.push(liquidateTokens[i]); + console.log(`Token ${liquidateTokens[i]} balance: ${balance.toString()}`); + } + } + } else { + shouldLiquidate = false; + } + + if (shouldLiquidate && pools.length > 0) { + let minAmountOut = await liquidateFee.estimateReturns(pools, amountsIn, destToken); + minAmountOut = minAmountOut.mul(BN.from(95)).div(BN.from(100)); // 5% off + console.log(`Liquidating ${pools.length} lp tokens`); + let dest = await MockToken.attach(destToken); + let destBalanceBefore = await dest.balanceOf(recipient); + await liquidateFee.liquidate(recipient, pools, amountsIn, destToken, tradeTokens, minAmountOut, { gasPrice: gasPrice }); + let destBalanceAfter = await dest.balanceOf(recipient); + console.log(`Liquidated, received amount: ${destBalanceAfter.sub(destBalanceBefore).toString()}`); + } + + exportAddresses(outputData); + console.log('setup completed'); + process.exit(0); + } +); + +function parseInput(jsonInput) { + admin = jsonInput["admin"]; + contractAddress = jsonInput["contractAddress"]; + recipient = jsonInput["recipient"]; + dmmRouter = jsonInput["dmmRouter"]; + destToken = jsonInput["destToken"]; + tokenConfigs = []; + let configs = jsonInput["tokenConfigs"]; + if (configs != undefined && configs.length > 0) { + for (let i = 0; i < configs.length; i++) { + let data = { + token: configs[i]["token"], + poolPath: configs[i]["poolPath"], + tokenPath: configs[i]["tokenPath"] + }; + tokenConfigs.push(data); + } + } + liquidateTokens = jsonInput["liquidateTokens"]; + tradeTokens = jsonInput["tradeTokens"]; + outputFilename = jsonInput["outputFilename"]; +} + +function exportAddresses(dictOutput) { + let json = JSON.stringify(dictOutput, null, 2); + fs.writeFileSync(path.join(__dirname, outputFilename), json); +} diff --git a/hardhat.config.ts b/hardhat.config.ts index 30a7937d..5f246bc4 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -14,6 +14,7 @@ dotenv.config(); import './deployment/katanaDeployment.js'; import './deployment/deployInternalGovernance.js'; import './deployment/liquidityMining/deployLiquidityMining.js'; +import './deployment/liquidateFee/deployLiquidateFeeWithKyberDMM.js'; import './deployment/createBinaryProposal.js'; import './deployment/simFullProposal.js'; import './deployment/simProposalExecution.js'; @@ -116,6 +117,18 @@ if (INFURA_API_KEY != '' && PRIVATE_KEY != '') { accounts: [PRIVATE_KEY], timeout: 20000, }; + + config.networks!.bsc_testnet = { + url: `https://data-seed-prebsc-1-s1.binance.org:8545/`, + accounts: [PRIVATE_KEY], + timeout: 20000, + }; + + config.networks!.bsc_mainnet = { + url: `https://bsc-dataseed.binance.org/`, + accounts: [PRIVATE_KEY], + timeout: 20000, + }; } if (ETHERSCAN_KEY != '' || POLYGONSCAN_KEY != '') { diff --git a/test/forking/liquidateFeeWithKyberDMM.ts b/test/forking/liquidateFeeWithKyberDMM.ts new file mode 100644 index 00000000..86107dfa --- /dev/null +++ b/test/forking/liquidateFeeWithKyberDMM.ts @@ -0,0 +1,130 @@ +import {ethers, waffle} from 'hardhat'; +import {BigNumber as BN} from '@ethersproject/bignumber'; + +import {expect} from 'chai'; +import Helper from '../helper'; + +const LiquidationHelper = require('./liquidationHelper'); + +import {MockToken__factory, LiquidateFeeWithKyberDMM__factory, LiquidateFeeWithKyberDMM} from '../../typechain'; + +let Token: MockToken__factory; +let LiquidateWithKyberDmm: LiquidateFeeWithKyberDMM__factory; + +const dmmRouter = LiquidationHelper.dmmRouterAddress; +const kncAddress = LiquidationHelper.kncAddress; +const wbtcAddress = LiquidationHelper.wbtcAddress; +const usdtAddress = LiquidationHelper.usdtAddress; +const wethAddress = LiquidationHelper.wethAddress; + +const poolAddresses = [ + LiquidationHelper.ethKncPoolAddress, + LiquidationHelper.ethWbtcPoolAddress, + LiquidationHelper.ethUsdtPoolAddress, +]; + +const srcTokens = [wethAddress, wbtcAddress, usdtAddress]; + +const tokenPath = [ + [wethAddress, kncAddress], + [wbtcAddress, wethAddress, kncAddress], + [usdtAddress, wethAddress, kncAddress], +]; + +const poolPath = [ + [LiquidationHelper.ethKncPoolAddress], + [LiquidationHelper.ethWbtcPoolAddress, LiquidationHelper.ethKncPoolAddress], + [LiquidationHelper.ethUsdtPoolAddress, LiquidationHelper.ethKncPoolAddress], +]; + +const tradeTokens = [wethAddress, usdtAddress, wbtcAddress]; + +let liquidateWithDmm: LiquidateFeeWithKyberDMM; + +describe('LiquidateWithKyberDMM-Forking', () => { + const [admin, user] = waffle.provider.getWallets(); + + before('reset state', async () => { + await Helper.resetForking(); + Token = (await ethers.getContractFactory('MockToken')) as MockToken__factory; + await LiquidationHelper.setupLpTokens(user); + LiquidateWithKyberDmm = (await ethers.getContractFactory( + 'LiquidateFeeWithKyberDMM' + )) as LiquidateFeeWithKyberDMM__factory; + liquidateWithDmm = await LiquidateWithKyberDmm.deploy(admin.address, user.address, dmmRouter); + await liquidateWithDmm.connect(admin).addOperator(user.address); + for (let i = 0; i < srcTokens.length; i++) { + await liquidateWithDmm.connect(user).setTradePath(srcTokens[i], kncAddress, tokenPath[i], poolPath[i]); + } + }); + + it('revert not operator', async () => { + await expect( + liquidateWithDmm.connect(admin).liquidate(user.address, [poolAddresses[0]], [1], kncAddress, tradeTokens, 0) + ).to.be.revertedWith('only operator'); + }); + + it('revert invalid length', async () => { + await expect( + liquidateWithDmm.connect(user).liquidate(user.address, poolAddresses, [1], kncAddress, tradeTokens, 0) + ).to.be.revertedWith('invalid lengths'); + }); + + it('revert not approve yet', async () => { + await expect( + liquidateWithDmm.connect(user).liquidate(user.address, [poolAddresses[0]], [1], kncAddress, tradeTokens, 0) + ).to.be.revertedWith('ERC20: transfer amount exceeds allowance'); + }); + + it('revert min amount too high', async () => { + let amounts = []; + for (let i = 0; i < poolAddresses.length; i++) { + let token = await Token.attach(poolAddresses[i]); + amounts.push(await token.balanceOf(user.address)); + await token.connect(user).approve(liquidateWithDmm.address, amounts[i]); + } + await expect( + liquidateWithDmm + .connect(user) + .liquidate(user.address, poolAddresses, amounts, kncAddress, tradeTokens, BN.from(2).pow(255)) + ).to.be.revertedWith('totalReturn < minReturn'); + }); + + it('revert path not set', async () => { + let pools = [LiquidationHelper.usdcUsdtPoolAddress]; + let amounts = []; + for (let i = 0; i < pools.length; i++) { + let token = await Token.attach(pools[i]); + amounts.push(await token.balanceOf(user.address)); + await token.connect(user).approve(liquidateWithDmm.address, amounts[i]); + } + await expect( + liquidateWithDmm + .connect(user) + .liquidate(user.address, pools, amounts, kncAddress, [LiquidationHelper.usdcAddress], 0) + ).to.be.revertedWith('DMMRouter: INVALID_PATH'); + }); + + it('liquidate LP tokens', async () => { + let amounts = []; + for (let i = 0; i < poolAddresses.length; i++) { + let token = await Token.attach(poolAddresses[i]); + amounts.push(await token.balanceOf(user.address)); + await token.connect(user).approve(liquidateWithDmm.address, amounts[i]); + } + + let kncToken = await Token.attach(kncAddress); + let kncBalanceBefore = await kncToken.balanceOf(user.address); + console.log(`KNC balance before: ${kncBalanceBefore.toString()}`); + let estimateReturnAmount = await liquidateWithDmm.estimateReturns(poolAddresses, amounts, kncAddress); + console.log(`Estimated amount KNC: ${estimateReturnAmount.toString()}`); + + let tx = await liquidateWithDmm + .connect(user) + .liquidate(user.address, poolAddresses, amounts, kncAddress, tradeTokens, 0); + + let kncBalanceAfter = await kncToken.balanceOf(user.address); + console.log(`KNC balance after: ${kncBalanceAfter.toString()}`); + console.log(`KNC delta: ${kncBalanceAfter.sub(kncBalanceBefore).toString()}`); + }); +}); diff --git a/test/forking/liquidationHelper.ts b/test/forking/liquidationHelper.ts index cd8d61db..688a3f52 100644 --- a/test/forking/liquidationHelper.ts +++ b/test/forking/liquidationHelper.ts @@ -36,6 +36,7 @@ const usdcAddress = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'; const daiAddress = '0x6b175474e89094c44da98b954eedeac495271d0f'; module.exports = { + dmmRouterAddress, ethKncPoolAddress, ethWbtcPoolAddress, ethUsdtPoolAddress,