Skip to content

Commit

Permalink
Add minimum purchase amount on liquidations (#114)
Browse files Browse the repository at this point in the history
* Implement the fix
* Fix existing test cases
* Add test cases for market patch
  • Loading branch information
superduck35 authored Nov 16, 2020
1 parent fcf0ca5 commit ac899d1
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 16 deletions.
6 changes: 4 additions & 2 deletions contracts/masset/liquidator/ILiquidator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ contract ILiquidator {
address _bAsset,
int128 _curvePosition,
address[] calldata _uniswapPath,
uint256 _trancheAmount
uint256 _trancheAmount,
uint256 _minReturn
)
external;

Expand All @@ -18,7 +19,8 @@ contract ILiquidator {
address _bAsset,
int128 _curvePosition,
address[] calldata _uniswapPath,
uint256 _trancheAmount
uint256 _trancheAmount,
uint256 _minReturn
)
external;

Expand Down
49 changes: 40 additions & 9 deletions contracts/masset/liquidator/Liquidator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { InitializableModule } from "../../shared/InitializableModule.sol";
import { ILiquidator } from "./ILiquidator.sol";
import { MassetHelpers } from "../../masset/shared/MassetHelpers.sol";

import { IBasicToken } from "../../shared/IBasicToken.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";
Expand Down Expand Up @@ -40,6 +41,7 @@ contract Liquidator is
uint256 private interval = 7 days;

mapping(address => Liquidation) public liquidations;
mapping(address => uint256) public minReturn;

struct Liquidation {
address sellToken;
Expand Down Expand Up @@ -85,14 +87,16 @@ contract Liquidator is
* @param _curvePosition Position of the bAsset in Curves MetaPool
* @param _uniswapPath The Uniswap path as an array of addresses e.g. [COMP, WETH, DAI]
* @param _trancheAmount The amount of bAsset units to buy in each weekly tranche
* @param _minReturn Minimum exact amount of bAsset to get for each (whole) sellToken unit
*/
function createLiquidation(
address _integration,
address _sellToken,
address _bAsset,
int128 _curvePosition,
address[] calldata _uniswapPath,
uint256 _trancheAmount
uint256 _trancheAmount,
uint256 _minReturn
)
external
onlyGovernance
Expand All @@ -103,7 +107,8 @@ contract Liquidator is
_integration != address(0) &&
_sellToken != address(0) &&
_bAsset != address(0) &&
_uniswapPath.length >= 2,
_uniswapPath.length >= 2 &&
_minReturn > 0,
"Invalid inputs"
);
require(_validUniswapPath(_sellToken, _bAsset, _uniswapPath), "Invalid uniswap path");
Expand All @@ -116,6 +121,7 @@ contract Liquidator is
lastTriggered: 0,
trancheAmount: _trancheAmount
});
minReturn[_integration] = _minReturn;

emit LiquidationModified(_integration);
}
Expand All @@ -127,13 +133,15 @@ contract Liquidator is
* @param _curvePosition Position of the bAsset in Curves MetaPool
* @param _uniswapPath The Uniswap path as an array of addresses e.g. [COMP, WETH, DAI]
* @param _trancheAmount The amount of bAsset units to buy in each weekly tranche
* @param _minReturn Minimum exact amount of bAsset to get for each (whole) sellToken unit
*/
function updateBasset(
address _integration,
address _bAsset,
int128 _curvePosition,
address[] calldata _uniswapPath,
uint256 _trancheAmount
uint256 _trancheAmount,
uint256 _minReturn
)
external
onlyGovernance
Expand All @@ -142,14 +150,16 @@ contract Liquidator is

address oldBasset = liquidation.bAsset;
require(oldBasset != address(0), "Liquidation does not exist");


require(_minReturn > 0, "Must set some minimum value");
require(_bAsset != address(0), "Invalid bAsset");
require(_validUniswapPath(liquidation.sellToken, _bAsset, _uniswapPath), "Invalid uniswap path");

liquidations[_integration].bAsset = _bAsset;
liquidations[_integration].curvePosition = _curvePosition;
liquidations[_integration].uniswapPath = _uniswapPath;
liquidations[_integration].trancheAmount = _trancheAmount;
minReturn[_integration] = _minReturn;

emit LiquidationModified(_integration);
}
Expand Down Expand Up @@ -180,6 +190,8 @@ contract Liquidator is
require(liquidation.bAsset != address(0), "Liquidation does not exist");

delete liquidations[_integration];
delete minReturn[_integration];

emit LiquidationEnded(_integration);
}

Expand All @@ -197,6 +209,9 @@ contract Liquidator is
function triggerLiquidation(address _integration)
external
{
// solium-disable-next-line security/no-tx-origin
require(tx.origin == msg.sender, "Must be EOA");

Liquidation memory liquidation = liquidations[_integration];

address bAsset = liquidation.bAsset;
Expand Down Expand Up @@ -234,19 +249,24 @@ contract Liquidator is
IERC20(sellToken).safeApprove(address(uniswap), 0);
IERC20(sellToken).safeApprove(address(uniswap), sellAmount);
// 3.2. Make the sale > https://uniswap.org/docs/v2/smart-contracts/router02/#swapexacttokensfortokens

// min amount out = sellAmount * priceFloor / 1e18
// e.g. 1e18 * 100e6 / 1e18 = 100e6
// e.g. 30e8 * 100e6 / 1e8 = 3000e6
// e.g. 30e18 * 100e18 / 1e18 = 3000e18
uint256 sellTokenDec = IBasicToken(sellToken).decimals();
uint256 minOut = sellAmount.mul(minReturn[_integration]).div(10 ** sellTokenDec);
require(minOut > 0, "Must have some price floor");
uniswap.swapExactTokensForTokens(
sellAmount,
0,
minOut,
uniswapPath,
address(this),
block.timestamp.add(1800)
);
uint256 bAssetBal = IERC20(bAsset).balanceOf(address(this));

// 3.3. Trade on Curve
IERC20(bAsset).safeApprove(address(curve), 0);
IERC20(bAsset).safeApprove(address(curve), bAssetBal);
uint256 purchased = curve.exchange_underlying(liquidation.curvePosition, 0, bAssetBal, 0);
uint256 purchased = _sellOnCrv(bAsset, liquidation.curvePosition);

// 4.0. Send to SavingsManager
address savings = _savingsManager();
Expand All @@ -256,4 +276,15 @@ contract Liquidator is

emit Liquidated(sellToken, mUSD, purchased, bAsset);
}

function _sellOnCrv(address _bAsset, int128 _curvePosition) internal returns (uint256 purchased) {
uint256 bAssetBal = IERC20(_bAsset).balanceOf(address(this));

IERC20(_bAsset).safeApprove(address(curve), 0);
IERC20(_bAsset).safeApprove(address(curve), bAssetBal);
uint256 bAssetDec = IBasicToken(_bAsset).decimals();
// e.g. 100e6 * 95e16 / 1e6 = 100e18
uint256 minOutCrv = bAssetBal.mul(95e16).div(10 ** bAssetDec);
purchased = curve.exchange_underlying(_curvePosition, 0, bAssetBal, minOutCrv);
}
}
12 changes: 10 additions & 2 deletions contracts/z_mocks/shared/MockCurveMetaPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,30 @@ contract MockCurveMetaPool is ICurveMetaPool {

address[] public coins;
address mUSD;
// number of out per in (scaled)
uint256 ratio = 98e16;


constructor(address[] memory _coins, address _mUSD) public {
require(_coins[0] == _mUSD, "Coin 0 must be mUSD");
coins = _coins;
mUSD = _mUSD;
}

function setRatio(uint256 _newRatio) external {
ratio = _newRatio;
}

// takes dx i from sender, returns j
function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 /*min_dy*/)
function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy)
external
returns (uint256)
{
require(j == 0, "Output must be mUSD");
address in_tok = coins[uint256(i)];
uint256 decimals = IBasicToken(in_tok).decimals();
uint256 out_amt = dx * (10 ** (18 - decimals));
uint256 out_amt = dx * (10 ** (18 - decimals)) * ratio / 1e18;
require(out_amt >= min_dy, "CRV: Output amount not enough");
IERC20(in_tok).transferFrom(msg.sender, address(this), dx);
IERC20(mUSD).transfer(msg.sender, out_amt);
return out_amt;
Expand Down
11 changes: 11 additions & 0 deletions contracts/z_mocks/shared/MockTrigger.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
pragma solidity 0.5.16;

import { ILiquidator } from "../../masset/liquidator/ILiquidator.sol";


contract MockTrigger {

function trigger(ILiquidator _liq, address _integration) external {
_liq.triggerLiquidation(_integration);
}
}
14 changes: 11 additions & 3 deletions contracts/z_mocks/shared/MockUniswap.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,17 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
// out token has 18 decimals
contract MockUniswap is IUniswapV2Router02 {

// how many tokens to give out for 1 in
uint256 ratio = 106;

function setRatio(uint256 _outRatio) external {
ratio = _outRatio;
}

// takes input from sender, produces output
function swapExactTokensForTokens(
uint amountIn,
uint /*amountOutMin*/,
uint amountOutMin,
address[] calldata path,
address /*to*/,
uint /*deadline*/
Expand All @@ -28,7 +34,9 @@ contract MockUniswap is IUniswapV2Router02 {
amounts[0] = amountIn;
IERC20(path[0]).transferFrom(msg.sender, address(this), amountIn);

uint256 output = amountIn * 106;
uint256 output = amountIn * ratio;
require(output >= amountOutMin, "UNI: Output amount not enough");

amounts[len-1] = output;
IERC20(path[len-1]).transfer(msg.sender, output);
}
Expand All @@ -41,7 +49,7 @@ contract MockUniswap is IUniswapV2Router02 {
view
returns (uint[] memory amounts)
{
uint256 amountIn = amountOut / 106;
uint256 amountIn = amountOut / ratio;
uint256 len = path.length;
amounts = new uint[](len);
amounts[0] = amountIn;
Expand Down
Loading

0 comments on commit ac899d1

Please sign in to comment.