From d2028261283b468f4e52d737ff72b7592746a6db Mon Sep 17 00:00:00 2001 From: alsco77 Date: Wed, 14 Oct 2020 15:13:54 +0200 Subject: [PATCH] Add full implementation flow --- contracts/masset/liquidator/ILiquidator.sol | 24 ++ contracts/masset/liquidator/Liquidator.sol | 210 ++++++++++-------- .../CompoundIntegration.sol | 87 ++------ .../TestLiquidatorContract.spec.ts.park} | 0 .../TestCompoundIntegration.spec.ts | 34 --- 5 files changed, 155 insertions(+), 200 deletions(-) create mode 100644 contracts/masset/liquidator/ILiquidator.sol rename test/{liquidator/TestLiquidatorContract.spec.ts => masset/liquidator/TestLiquidatorContract.spec.ts.park} (100%) diff --git a/contracts/masset/liquidator/ILiquidator.sol b/contracts/masset/liquidator/ILiquidator.sol new file mode 100644 index 00000000..f8645e7e --- /dev/null +++ b/contracts/masset/liquidator/ILiquidator.sol @@ -0,0 +1,24 @@ +pragma solidity 0.5.16; + + + +contract ILiquidator { + + enum LendingPlatform { Null, Compound, Aave } + + function createLiquidation( + address _integration, + LendingPlatform _lendingPlatform, + address _sellToken, + address _bAsset, + address[] calldata _uniswapPath, + uint256 _sellTranche + ) external; + function updateBasset(address _bAsset, address[] calldata _uniswapPath) external; + function deleteLiquidation(address _integration) external; + function changeTrancheAmount(uint256 _sellTranche) external; + + function triggerLiquidation(address _integration) external; + + function collect() external; +} \ No newline at end of file diff --git a/contracts/masset/liquidator/Liquidator.sol b/contracts/masset/liquidator/Liquidator.sol index c9d191b6..9ac57225 100644 --- a/contracts/masset/liquidator/Liquidator.sol +++ b/contracts/masset/liquidator/Liquidator.sol @@ -7,12 +7,14 @@ import { IPlatformIntegration } from "../../interfaces/IPlatformIntegration.sol" import { Initializable } from "@openzeppelin/upgrades/contracts/Initializable.sol"; import { InitializableModule } from "../../shared/InitializableModule.sol"; +import { ILiquidator } from "./ILiquidator.sol"; import { MassetHelpers } from "../../masset/shared/MassetHelpers.sol"; import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + /** * @title Liquidator * @author Stability Labs Pty. Ltd. @@ -22,6 +24,7 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; * DATE: 2020-10-13 */ contract Liquidator is + ILiquidator, Initializable, InitializableModule { @@ -37,15 +40,18 @@ contract Liquidator is mapping(address => Liquidation) public liquidations; - enum LendingPlatform { Null, Compound, Aave } - struct Liquidation { LendingPlatform platform; + address sellToken; + address bAsset; + address pToken; address[] uniswapPath; + + uint256 collectUnits; // Minimum collection amount for the integration, updated after liquidation uint256 lastTriggered; - uint256 trancheAmount; + uint256 sellTranche; // Tranche amount, with token decimals } /** @dev Constructor */ @@ -72,7 +78,7 @@ contract Liquidator is * @param _sellToken The integration contract address for the _bAsset * @param _bAsset The _bAsset address that this liquidation is for * @param _uniswapPath The Uniswap path as an array of addresses e.g. [COMP, WETH, DAI] - * @param _trancheAmount The amount of tokens to be sold when triggered + * @param _sellTranche The amount of tokens to be sold when triggered (in token decimals) */ function createLiquidation( address _integration, @@ -80,7 +86,7 @@ contract Liquidator is address _sellToken, address _bAsset, address[] calldata _uniswapPath, - uint256 _trancheAmount + uint256 _sellTranche ) external onlyGovernance @@ -89,11 +95,12 @@ contract Liquidator is require( _integration != address(0) && _lendingPlatform != LendingPlatform.Null && - _sellToken != address(0), - _bAsset != address(0), + _sellToken != address(0) && + _bAsset != address(0) && _uniswapPath.length >= uint(2), "Invalid inputs" ); + require(_validUniswapPath(_sellToken, _bAsset, _uniswapPath), "Invalid uniswap path"); address pToken = IPlatformIntegration(_integration).bAssetToPToken(_bAsset); require(pToken != address(0), "no pToken for this bAsset"); @@ -102,31 +109,30 @@ contract Liquidator is platform: _lendingPlatform, sellToken: _sellToken, bAsset: _bAsset, + pToken: pToken, uniswapPath: _uniswapPath, - lastTriggered: uint256(0), - trancheAmount: _trancheAmount + collectUnits: 0, + lastTriggered: 0, + sellTranche: _sellTranche }); - _giveApproval(_integration, pToken); + if (_lendingPlatform == LendingPlatform.Compound) { + MassetHelpers.safeInfiniteApprove(_bAsset, pToken); + } emit LiquidationModified(_integration); } - function _giveApproval(address _integration, address _pToken) internal { - - // 1. Approve integration to collect pToken (bAsset or pToken change) - // 2. Approve cToken to mint (bAsset or pToken change) - - Liquidation memory liquidation = liquidations[_integration]; - - MassetHelpers.safeInfiniteApprove(_pToken, _integration); - - if (liquidation.platform == LendingPlatform.Compound) { - MassetHelpers.safeInfiniteApprove(liquidation.bAsset, _pToken); - } + function _validUniswapPath(address _sellToken, address _bAsset, address[] memory _uniswapPath) + internal + view + returns (bool) + { + uint256 len = _uniswapPath.length; + return _sellToken == _uniswapPath[0] && _bAsset == _uniswapPath[len]; } - function changeBasset( + function updateBasset( address _bAsset, address[] calldata _uniswapPath ) @@ -145,7 +151,7 @@ contract Liquidator is } function changeTrancheAmount( - uint256 _trancheAmount + uint256 _sellTranche ) external onlyGovernance @@ -161,17 +167,17 @@ contract Liquidator is external onlyGovernance { - Liquidation memory liquidation = liquidations[_integration]; - require(liquidation.bAsset != address(0), "No liquidation for this integration"); + // Liquidation memory liquidation = liquidations[_integration]; + // require(liquidation.bAsset != address(0), "No liquidation for this integration"); - // todo - // 1. Deal will old bAsset (if changed) - // > transfer remainer of pToken to integration - // > remove approval for both bAsset and pToken + // // todo + // // 1. Deal will old bAsset (if changed) + // // > transfer remainer of pToken to integration + // // > remove approval for both bAsset and pToken - delete liquidations[_integration]; - emit LiquidationEnded(_integration); + // delete liquidations[_integration]; + // emit LiquidationEnded(_integration); } /*************************************** @@ -183,110 +189,120 @@ contract Liquidator is external { Liquidation memory liquidation = liquidations[_integration]; + address bAsset = liquidation.bAsset; require(bAsset != address(0), "Liquidation does not exist"); - require(block.timestamp > liquidation.lastTriggered.add(interval), "Must wait for interval"); - liquidation.lastTriggered = block.timestamp; + require(block.timestamp > liquidation.lastTriggered.add(interval), "Must wait for interval"); + liquidations[_integration].lastTriggered = block.timestamp; // Cache variables address sellToken = liquidation.sellToken; - address integration = liquidation.integration; + address[] memory uniswapPath = liquidation.uniswapPath; - // Transfer sellTokens from integration contract if there are some - // Assumes infinite approval - uint256 integrationBal = IERC20(sellToken).balanceOf(integration); + // 1. Transfer sellTokens from integration contract if there are some + // Assumes infinite approval + uint256 integrationBal = IERC20(sellToken).balanceOf(_integration); if (integrationBal > 0) { - IERC20(sellToken).safeTransferFrom(integration, address(this), integrationBal); + IERC20(sellToken).safeTransferFrom(_integration, address(this), integrationBal); } - // Check contract balance - uint256 bal = IERC20(sellToken).balanceOf(address(this)); - require((bal > 0), "No sell tokens to liquidate"); - - // Get the amount to sell based on the tranche amount we want to buy - (uint256 amountToSell, uint256 expectedAmount) = getAmountToSell(liquidation.uniswapPath, liquidation.trancheAmount); + // 2. Get the amount to sell based on the tranche amount we want to buy + // Check contract balance + uint256 sellTokenBal = IERC20(sellToken).balanceOf(address(this)); + require(sellTokenBal > 0, "No sell tokens to liquidate"); + // Calc amounts for max tranche + uint[] memory amountsIn = IUniswapV2Router02(uniswapAddress).getAmountsIn(liquidation.sellTranche, uniswapPath); + uint256 sellAmount = amountsIn[0]; - // The minimum amount of output tokens that must be received for the transaction not to revert - // Set to 80% of expected - uint256 minAcceptable = expectedAmount.mul(uint(8000)).div(uint(10000)); - - // Sell amountToSell unless balance is lower in which case sell everything and relax acceptable check - uint256 sellAmount; - if (bal > amountToSell) { - sellAmount = amountToSell; - } else { - sellAmount = bal; - minAcceptable = 0; + if (sellTokenBal < sellAmount) { + sellAmount = sellTokenBal; } - // Approve Uniswap and make the swap - // https://uniswap.org/docs/v2/smart-contracts/router02/#swapexacttokensfortokens + // 3. Make the swap + // 3.1 Approve Uniswap and make the swap IERC20(sellToken).safeApprove(uniswapAddress, 0); - IERC20(sellToken).safeApprove(uniswapAddress, amountToSell); + IERC20(sellToken).safeApprove(uniswapAddress, sellAmount); + + // 3.2. Make the sale > https://uniswap.org/docs/v2/smart-contracts/router02/#swapexacttokensfortokens IUniswapV2Router02(uniswapAddress).swapExactTokensForTokens( sellAmount, - minAcceptable, - liquidation.uniswapPath, + 0, + uniswapPath, address(this), block.timestamp.add(1800) ); + uint256 bAssetBal = IERC20(bAsset).balanceOf(address(this)); + + // 4. Deposit to lending platform + // Assumes integration contracts have inifinte approval to collect them + if (liquidation.platform == LendingPlatform.Compound) { + // 4.1. Exec deposit + ICERC20 cToken = ICERC20(liquidation.pToken); + require(cToken.mint(bAssetBal) == 0, "cToken mint failed"); - // Deposit to lending platform - // Assumes integration contracts have inifinte approval to collect them - if (liquidation.lendingPlatform == LendingPlatform.Compound) { - depositToCompound(liquidation.pToken, _bAsset); + // 4.2. Set minCollect to 25% of received + uint256 cTokenBal = cToken.balanceOf(address(this)); + liquidations[_integration].collectUnits = cTokenBal.mul(2).div(10); } else { revert("Lending Platform not supported"); } - emit LiquidationTriggered(_bAsset); - } - - /** - * @dev Deposits to Compound - * @param _pToken The _pToken to mint - * @param _bAsset The _bAsset liquidation to be triggered - */ - function depositToCompound(address _pToken, address _bAsset) - internal - { - uint256 bAssetBalance = IERC20(_bAsset).balanceOf(address(this)); - require((bAssetBalance > 0), "No tokens to deposit"); - require(ICERC20(_pToken).mint(bAssetBalance) == 0, "cToken mint failed"); + emit Liquidated(sellToken, bAsset, bAssetBal); } /** * @dev Get the amount of sellToken to be sold for a number of bAsset * @param _uniswapPath The Uniswap path for this liquidation - * @param _trancheAmount The tranche size that we want to buy each time + * @param _sellTranche The tranche size that we want to buy each time */ - function getAmountToSell( - address[] memory _uniswapPath, - uint256 _trancheAmount + function _getAmountToSell( + address[] memory _uniswapPath, + uint256 _sellTranche ) - internal view returns (uint256, uint256) + internal + view + returns (uint256, uint256) { - // The _trancheAmount is the number of bAsset we want to buy each time - // DAI has 18 decimals so 1000 DAI is 10*10^18 or 1000000000000000000000 - // This value is set when adding the liquidation - // We randomize this amount by buying betwen 80% and 95% of the amount. - // Uniswap gives us the amount we need to sell with `getAmountsIn`. - uint256 randomBasisPoint = uint256(blockhash(block.number-1)).mod(uint(1500)).add(uint(8000)); - uint256 amountWanted = _trancheAmount.mul(randomBasisPoint).div(uint(10000)); + // // The _sellTranche is the number of bAsset we want to buy each time + // // DAI has 18 decimals so 1000 DAI is 10*10^18 or 1000000000000000000000 + // // This value is set when adding the liquidation + // // We randomize this amount by buying betwen 80% and 95% of the amount. + // // Uniswap gives us the amount we need to sell with `getAmountsIn`. + // uint256 randomBasisPoint = uint256(blockhash(block.number-1)).mod(uint(1500)).add(uint(8000)); + // uint256 amountWanted = _sellTranche.mul(randomBasisPoint).div(uint(10000)); - // Returns the minimum input asset amount required to buy - // the given output asset amount (accounting for fees) given reserves - // https://uniswap.org/docs/v2/smart-contracts/router02/#getamountsin - uint[] memory amountsIn = IUniswapV2Router02(uniswapAddress).getAmountsIn(amountWanted, _uniswapPath); + // // Returns the minimum input asset amount required to buy + // // the given output asset amount (accounting for fees) given reserves + // // https://uniswap.org/docs/v2/smart-contracts/router02/#getamountsin + // uint[] memory amountsIn = IUniswapV2Router02(uniswapAddress).getAmountsIn(amountWanted, _uniswapPath); - return (amountsIn[0], amountWanted); + // return (amountsIn[0], amountWanted); } /*************************************** - CLAIM + COLLECT ****************************************/ + function collect() + external + { + Liquidation memory liquidation = liquidations[msg.sender]; + address pToken = liquidation.pToken; + if(pToken != address(0)){ + uint256 bal = IERC20(pToken).balanceOf(address(this)); + if (bal > 0) { + // If we are below the threshold transfer the entire balance + // otherwise send between 5 and 35% + if (bal > liquidation.collectUnits) { + bytes32 bHash = blockhash(block.number - 1); + uint256 randomBp = uint256(keccak256(abi.encodePacked(block.timestamp, bHash))).mod(3e4).add(5e3); + bal = bal.mul(randomBp).div(1e5); + } + IERC20(pToken).transfer(msg.sender, bal); + } + } + } } diff --git a/contracts/masset/platform-integrations/CompoundIntegration.sol b/contracts/masset/platform-integrations/CompoundIntegration.sol index ac75106d..0686ae57 100644 --- a/contracts/masset/platform-integrations/CompoundIntegration.sol +++ b/contracts/masset/platform-integrations/CompoundIntegration.sol @@ -1,5 +1,6 @@ pragma solidity 0.5.16; +import { ILiquidator } from "../liquidator/Liquidator.sol"; import { ICERC20 } from "./ICompound.sol"; import { CommonHelpers } from "../../shared/CommonHelpers.sol"; import { InitializableAbstractIntegration, MassetHelpers, IERC20 } from "./InitializableAbstractIntegration.sol"; @@ -25,41 +26,22 @@ contract CompoundIntegration is InitializableAbstractIntegration { ****************************************/ /** - * @dev Collects the accumulated COMP token from the contract - * @param _recipient Recipient to credit + * @dev Approves Liquidator to spend reward tokens */ - function collectRewardToken( - address _recipient - ) + function approveRewardToken() external onlyGovernor { + address liquidator = nexus.getModule(keccak256("Liquidator")); + require(liquidator != address(0), "Liquidator address cannot be zero"); + // Official checksummed COMP token address // https://ethplorer.io/address/0xc00e94cb662c3520282e6f5717214004a7f26888 - IERC20 compToken = IERC20(0xc00e94Cb662C3520282E6f5717214004A7f26888); - - uint256 balance = compToken.balanceOf(address(this)); - - require(compToken.transfer(_recipient, balance), "Collection transfer failed"); - - emit RewardTokenCollected(_recipient, balance); - } + address compToken = address(0xc00e94Cb662C3520282E6f5717214004A7f26888); - /** - * @dev Approves Liquidator to spend reward tokens - * @param _rewardToken Reward token - */ - function approveRewardToken( - address _rewardToken - ) - external - onlyGovernor - { - address liq = nexus.getModule(keccak256("Liquidator")); - require(liq != address(0), "Liquidator address cannot be zero"); - MassetHelpers.safeInfiniteApprove(_rewardToken, liq); + MassetHelpers.safeInfiniteApprove(compToken, liquidator); - emit RewardTokenApproved(_rewardToken, liq); + emit RewardTokenApproved(address(compToken), liquidator); } /*************************************** @@ -266,60 +248,27 @@ contract CompoundIntegration is InitializableAbstractIntegration { amount = _underlying.mul(1e18).div(exchangeRate); } + /*************************************** + HELPERS + ****************************************/ + /** - * @dev Checks whether a claim should be made + * @dev Claims proceeds from the liquidated COMP, if enough time has passed * This compares the block.timestamp with a somewhat random time * Adds randomness by muliplying the 1 hour delay between 1x and 3x */ function _claimLiquidated() internal { - uint256 salt = uint256(keccak256(abi.encodePacked(blockhash(block.number)))).mod(3e8); - uint256 timeDelay = uint256(1 hours).mul(salt).div(1e8).add(6 hours); + bytes32 bHash = blockhash(block.number - 1); + uint256 salt = uint256(keccak256(abi.encodePacked(block.timestamp, bHash))).mod(3e6); + uint256 timeDelay = uint256(1 hours).mul(salt).div(1e6).add(6 hours); if (block.timestamp > lastClaimed.add(timeDelay)) { lastClaimed = block.timestamp; address liquidator = nexus.getModule(keccak256("Liquidator")); if(liquidator != address(0)){ - ILiquidator(liquidator).claim(); - } - } - } - - /** - * @dev Collects pTokens from the Liquidator - * Adds randomness by computing a basis point between 1000 and 4000 - * This correlates to transfering 10%-40% of the total balance - * @param _bAsset Address for the bAsset - */ - function _claim(address _bAsset) - internal - { - lastClaimed = block.timestamp; - - address liquidator = nexus.getModule(keccak256("Liquidator")); - require(liquidator != address(0), "Liquidator address cannot be zero"); - - address cToken = bAssetToPToken[_bAsset]; - require(cToken != address(0), "cToken does not exist"); - - uint256 liquidatorBal = IERC20(cToken).balanceOf(liquidator); - - if (liquidatorBal > 0) { - // Set a threshold of 1000*10^decimals for the bAsset - uint256 bAssetDecimals = CommonHelpers.getDecimals(_bAsset); - uint256 bAssetThreshold = uint(1000).mul(uint(10)**bAssetDecimals); - // Convert to cTokens - ICERC20 cTokenWrapped = _getCTokenFor(_bAsset); - uint256 threshold = _convertUnderlyingToCToken(cTokenWrapped, bAssetThreshold); - if (liquidatorBal < threshold) { - // if we are below the threshold transfer the entire balance - IERC20(cToken).safeTransferFrom(liquidator, address(this), liquidatorBal); - } else { - // transfer between 10% and 40% of allowance - uint256 randomBp = uint256(blockhash(block.number-1)).mod(uint(3000)).add(uint(1000)); - uint256 toTransfer = liquidatorBal.mul(randomBp).div(uint(10000)); - IERC20(cToken).safeTransferFrom(liquidator, address(this), toTransfer); + ILiquidator(liquidator).collect(); } } } diff --git a/test/liquidator/TestLiquidatorContract.spec.ts b/test/masset/liquidator/TestLiquidatorContract.spec.ts.park similarity index 100% rename from test/liquidator/TestLiquidatorContract.spec.ts rename to test/masset/liquidator/TestLiquidatorContract.spec.ts.park diff --git a/test/masset/platform-integrations/TestCompoundIntegration.spec.ts b/test/masset/platform-integrations/TestCompoundIntegration.spec.ts index ff6a1599..5ffbe0f8 100644 --- a/test/masset/platform-integrations/TestCompoundIntegration.spec.ts +++ b/test/masset/platform-integrations/TestCompoundIntegration.spec.ts @@ -839,40 +839,6 @@ contract("CompoundIntegration", async (accounts) => { }); }); - describe("collectRewardToken", async () => { - before(async () => { - await runSetup(false, true); - }); - - // This needs to be tested on a mainnet fork instead, as the address is hardcoded. - // To test here, one would need to override the function and supply a dummy address - it("should allow the governor to collect any outstanding COMP", async () => { - if (!systemMachine.isGanacheFork) return; - - const COMP = await c_MockERC20.at(ma.COMP); - - const bal = await COMP.balanceOf(d_CompoundIntegration.address); - expect(bal.gtn(0), "Must have non zero COMP bal"); - - const tx = await d_CompoundIntegration.collectRewardToken(sa.dummy1, { - from: sa.governor, - }); - - expectEvent(tx.receipt, "RewardTokenCollected", { - recipient: sa.dummy1, - amount: bal, - }); - }); - it("should fail if not called by the governor", async () => { - await expectRevert( - d_CompoundIntegration.collectRewardToken(sa.dummy1, { - from: sa.dummy1, - }), - "Only governor can execute", - ); - }); - }); - describe("checkBalance", async () => { beforeEach(async () => { await runSetup(false, true);