/ contracts / finance / collateral / BaseCollateralManager.sol
BaseCollateralManager.sol
  1  pragma solidity >=0.6.0;
  2  pragma experimental ABIEncoderV2;
  3  
  4  import "../../deployment/Deployer.sol";
  5  import "../../deployment/ManagedContract.sol";
  6  import "../../interfaces/IProtocolSettings.sol";
  7  import "../../interfaces/UnderlyingFeed.sol";
  8  import "../../interfaces/IGovernableLiquidityPool.sol";
  9  import "../../interfaces/ICreditProvider.sol";
 10  import "../../interfaces/IOptionsExchange.sol";
 11  import "../../interfaces/IOptionToken.sol";
 12  import "../../interfaces/IUnderlyingVault.sol";
 13  import "../../interfaces/IUnderlyingCreditProvider.sol";
 14  import "../../interfaces/IBaseCollateralManager.sol";
 15  import "../../interfaces/IUniswapV2Router01.sol";
 16  import "../../utils/SafeCast.sol";
 17  import "../../utils/MoreMath.sol";
 18  import "../../utils/Decimal.sol";
 19  import "../../utils/Convert.sol";
 20  import "../../utils/SafeERC20.sol";
 21  
 22  abstract contract BaseCollateralManager is ManagedContract, IBaseCollateralManager {
 23  
 24      using SafeCast for uint;
 25      using SafeMath for uint;
 26      using SignedSafeMath for int;
 27      using Decimal for Decimal.D256;
 28      using SafeERC20 for IERC20_2;
 29  
 30      
 31      IUnderlyingVault private vault;
 32      IProtocolSettings internal settings;
 33      ICreditProvider internal creditProvider;
 34      IOptionsExchange internal exchange;
 35  
 36      uint private timeBase;
 37      uint private sqrtTimeBase;
 38      uint private collateralCallPeriod;
 39      uint internal _volumeBase;
 40  
 41      mapping(address => mapping(address => uint256)) private writerCollateralCall;
 42  
 43      event LiquidateEarly(
 44          address indexed token,
 45          address indexed sender,
 46          address indexed onwer,
 47          uint volume
 48      );
 49  
 50      event CollateralCall(
 51          address indexed token,
 52          address indexed sender,
 53          address indexed onwer,
 54          uint volume
 55      );
 56  
 57      event LiquidateExpired(
 58          address indexed token,
 59          address indexed sender,
 60          address indexed onwer,
 61          uint volume
 62      );
 63  
 64      event DebtSwap(
 65          address indexed tokenA,
 66          address indexed tokenB,
 67          uint volume
 68      );
 69  
 70      function initialize(Deployer deployer) virtual override internal {
 71  
 72          creditProvider = ICreditProvider(deployer.getContractAddress("CreditProvider"));
 73          settings = IProtocolSettings(deployer.getContractAddress("ProtocolSettings"));
 74          exchange = IOptionsExchange(deployer.getContractAddress("OptionsExchange"));
 75          vault = IUnderlyingVault(deployer.getContractAddress("UnderlyingVault"));
 76  
 77          _volumeBase = 1e18;
 78          timeBase = 1e18;
 79          sqrtTimeBase = 1e9;
 80          collateralCallPeriod = 1 days;
 81      }
 82  
 83      function collateralSkew(address underlyingFeed) private view returns (int) {
 84          // core across all collateral models
 85          /*
 86              This allows the exchange to split any excess credit balance (due to debt) onto any new deposits while still holding debt balance for an individual account;
 87          */
 88  
 89          address underlying = UnderlyingFeed(underlyingFeed).getUnderlyingAddr();
 90          if (underlying == address(0)) {
 91              return 0;
 92          }
 93          
 94          (,int answer) = UnderlyingFeed(underlyingFeed).getLatestPrice();
 95          address udlCdtp = vault.getUnderlyingCreditProvider(underlying);
 96          int totalUnderlyingBalance = int(IUnderlyingCreditProvider(udlCdtp).totalTokenStock()); // underlying token balance
 97          int totalUnderlyingCreditBalance = int(IUnderlyingCreditProvider(udlCdtp).getTotalBalance()); // credit balance
 98          int totalOwners = int(creditProvider.getTotalOwners()).add(1);
 99          //need to multiple by latest price of asset to normalize into exchange balance amount
100          int skew = totalUnderlyingCreditBalance.sub(totalUnderlyingBalance).mul(answer).div(int(_volumeBase));
101  
102          // try to split between if short underlying tokens
103          return skew.div(totalOwners);  
104      }
105  
106      function collateralSkewForPositionUnderlying(int coll, address underlyingFeed) internal view returns (int) {
107          // core across all collateral models, only apply residual
108          int modColl;
109          int skew = collateralSkew(underlyingFeed);
110          Decimal.D256 memory skewPct;
111          if (skew != 0){
112              skewPct = Decimal.ratio(uint(coll), MoreMath.abs(skew));
113          } else {
114              skewPct = Decimal.zero();
115          }
116  
117          if (skewPct.greaterThanOrEqualTo(Decimal.one())) {
118              modColl = skew;
119          } else {
120              // shortag per addr exceeds underlying collateral reqs, only add percentage increase of underlying collateral reqs, never reduce collateral requirements for positions covered by underlying
121  
122              int modSkew = int(Decimal.mul(skewPct, uint(coll)).asUint256());
123              modColl = (skew >= 0) ? modSkew : 0;
124          }
125  
126          return modColl;
127      }
128  
129      function collateralSkew() private view returns (int) {
130          // core across all collateral models
131          /*
132              This allows the exchange to split any excess credit balance (due to debt) onto any new deposits while still holding debt balance for an individual account 
133                  OR
134              split any excess stablecoin balance (due to more collected from debt than debt outstanding) to discount any new deposits()
135          */
136          int totalStableCoinBalance = int(creditProvider.totalTokenStock()); // stable coin balance
137          int totalCreditBalance = int(creditProvider.getTotalBalance()); // credit balance
138          int totalOwners = int(creditProvider.getTotalOwners()).add(1);
139          int skew = totalCreditBalance.sub(totalStableCoinBalance);
140  
141          // try to split between if short stable coins
142          return skew.div(totalOwners);  
143      }
144  
145      function collateralSkewForPosition(int coll) internal view returns (int) {
146          // core across all collateral models
147          int modColl;
148          int skew = collateralSkew();
149          Decimal.D256 memory skewPct;
150          if (skew != 0){
151              skewPct = Decimal.ratio(uint(coll), MoreMath.abs(skew));
152          } else {
153              skewPct = Decimal.zero();
154          }
155  
156          if (skewPct.greaterThanOrEqualTo(Decimal.one())) {
157              modColl = coll.add(skew);
158          } else {
159              // shortage/surplus per addr exceeds underlying collateral reqs, only add/sub percentage increase of underlying collateral reqs
160  
161              int modSkew = int(Decimal.mul(skewPct, uint(coll)).asUint256());
162              modColl = (skew >= 0) ? coll.add(modSkew) : coll.sub(modSkew);
163          }
164  
165          return modColl;
166      }
167  
168      function calcExpectedPayout(address owner) override external view returns (int payout) {
169          // multi udl feed refs, need to make core accross all collateral models
170          (,address[] memory _tokens, uint[] memory _holding, uint[] memory _written,, int[] memory _iv,) = exchange.getBook(owner);
171  
172          for (uint i = 0; i < _tokens.length; i++) {
173              int price = queryPoolPrice(owner, IOptionToken(_tokens[i]).name());
174              payout = payout.add(
175                  (price != 0 ? price : _iv[i]).mul(
176                      int(_holding[i]).sub(int(_written[i]))
177                  )
178              );
179          }
180  
181          payout = payout.div(int(_volumeBase));
182      }
183  
184      function calcCollateralInternal(address owner, bool is_regular) virtual internal view returns (int);
185  
186      function calcNetCollateralInternal(address[] memory _tokens, uint[] memory _uncovered, uint[] memory _holding, bool is_regular)  virtual internal view returns (int);
187  
188      function calcLiquidationVolume(
189          address owner,
190          IOptionsExchange.OptionData memory opt,
191          address _tk,
192          IOptionsExchange.FeedData memory fd,
193          uint written
194      )
195          private
196          returns (uint volume)
197      {    
198          uint bal = creditProvider.balanceOf(owner);
199          uint coll = calcCollateral(owner, true);
200  
201          if (coll > bal) {
202              if (writerCollateralCall[owner][_tk] != 0) {
203                  // cancel collateral call
204                  writerCollateralCall[owner][_tk] = 0;
205              }
206          }
207          require(coll > bal, "Collateral Manager: unfit for liquidation");
208          
209          volume = coll.sub(bal).mul(_volumeBase).mul(written).div(
210              calcCollateral(
211                  uint(fd.upperVol).sub(uint(fd.lowerVol)),
212                  written,
213                  opt
214              )
215          );
216  
217          volume = MoreMath.min(volume, written);
218      }
219  
220      function calcLiquidationValue(
221          IOptionsExchange.OptionData memory opt,
222          uint vol,
223          uint written,
224          uint volume,
225          uint iv
226      )
227          private
228          view
229          returns (uint value)
230      {    
231          value = calcCollateral(vol, written, opt).add(iv).mul(volume).div(written);
232      }
233  
234      function calcIntrinsicValue(IOptionsExchange.OptionData memory opt) override public view returns (int value) {
235          
236          int udlPrice = getUdlPrice(opt);
237          int strike = int(opt.strike);
238  
239          if (opt._type == IOptionsExchange.OptionType.CALL) {
240              value = MoreMath.max(0, udlPrice.sub(strike));
241          } else if (opt._type == IOptionsExchange.OptionType.PUT) {
242              value = MoreMath.max(0, strike.sub(udlPrice));
243          }
244      }
245  
246      function queryPoolPrice(
247          address poolAddr,
248          string memory symbol
249      )
250          override public
251          view
252          returns (int)
253      {
254          uint price = 0;
255          IGovernableLiquidityPool pool = IGovernableLiquidityPool(poolAddr);
256          
257  
258          try pool.queryBuy(symbol, true) returns (uint _buyPrice, uint) {
259              price = price.add(_buyPrice);
260          } catch (bytes memory /*lowLevelData*/) {
261              return 0;
262          }
263  
264          try pool.queryBuy(symbol, false) returns (uint _sellPrice, uint) {
265              price = price.add(_sellPrice);
266          } catch (bytes memory /*lowLevelData*/) {
267              return 0;
268          }
269  
270          return int(price).div(2);
271      }
272  
273      function getFeedData(address udlFeed) internal view returns (IOptionsExchange.FeedData memory fd) {
274          UnderlyingFeed feed = UnderlyingFeed(udlFeed);
275  
276          uint vol = feed.getDailyVolatility(settings.getVolatilityPeriod());
277  
278          fd = IOptionsExchange.FeedData(
279              feed.calcLowerVolatility(uint(vol)).toUint120(),
280              feed.calcUpperVolatility(uint(vol)).toUint120()
281          );
282      }
283  
284      function calcCollateral(
285          IOptionsExchange.OptionData calldata opt,
286          uint volume
287      ) override virtual external view returns (uint);
288      
289      function calcCollateral(uint vol, uint volume, IOptionsExchange.OptionData memory opt) internal view returns (uint) {
290          
291          return (vol.mul(volume).mul(
292              MoreMath.sqrt(daysToMaturity(opt)))
293          ).div(sqrtTimeBase);
294      }
295  
296      function liquidateExpired(address _tk, address[] calldata owners) override external {
297  
298          IOptionsExchange.OptionData memory opt = exchange.getOptionData(_tk);
299          IOptionToken tk = IOptionToken(_tk);
300          require(getUdlNow(opt) >= opt.maturity, "Collateral Manager: option not expired");
301          uint iv = uint(calcIntrinsicValue(opt));
302  
303          for (uint i = 0; i < owners.length; i++) {
304              liquidateOptions(owners[i], opt, tk, true, iv);
305          }
306      }
307  
308      function liquidateOptions(address _tk, address owner) override external returns (uint value) {
309          
310          IOptionsExchange.OptionData memory opt = exchange.getOptionData(_tk);
311          require(opt.udlFeed != address(0), "invalid token");
312  
313          IOptionToken tk = IOptionToken(_tk);
314          require(tk.writtenVolume(owner) > 0, "Collateral Manager: invalid owner");
315  
316          bool isExpired = getUdlNow(opt) >= opt.maturity;
317          uint iv = uint(calcIntrinsicValue(opt));
318          
319          value = liquidateOptions(owner, opt, tk, isExpired, iv);
320      }
321  
322      function liquidateOptions(
323          address owner,
324          IOptionsExchange.OptionData memory opt,
325          IOptionToken tk,
326          bool isExpired,
327          uint iv
328      )
329          private
330          returns (uint value)
331      {
332          uint written = isExpired ?
333              tk.writtenVolume(owner) :
334              tk.uncoveredVolume(owner);
335          iv = iv.mul(written);
336  
337          if (isExpired) {
338              value = liquidateAfterMaturity(owner, tk, opt.udlFeed, written, iv);
339              emit LiquidateExpired(address(tk), msg.sender, owner, written);
340          } else {
341              require(written > 0, "Collateral Manager: invalid volume");
342              value = liquidateBeforeMaturity(owner, opt, tk, written, iv);
343          }
344      }
345  
346      function liquidateAfterMaturity(
347          address owner,
348          IOptionToken tk,
349          address feed,
350          uint written,
351          uint iv
352      )
353          private
354          returns (uint value)
355      {
356  
357          // if borrowed liquidty was used to write options need to debit it from pool addr
358          creditProvider.processIncentivizationPayment(msg.sender, settings.getBaseIncentivisation());
359          creditProvider.nullOptionBorrowBalance(address(tk), owner);
360  
361          if (iv > 0) {
362              value = iv.div(_volumeBase);
363              vault.liquidate(owner, address(tk), feed, value);
364              creditProvider.processPayment(owner, address(tk), value);
365          }
366  
367          vault.release(owner, address(tk), feed, uint(-1));
368  
369          if (written > 0) {
370              exchange.burn(owner, written, address(tk));
371          }
372      }
373  
374      function liquidateBeforeMaturity(
375          address owner,
376          IOptionsExchange.OptionData memory opt,
377          IOptionToken tk,
378          uint written,
379          uint iv
380      )
381          private
382          returns (uint value)
383      {
384          IOptionsExchange.FeedData memory fd = getFeedData(opt.udlFeed);
385          address tkAddr = address(tk);
386          uint volume = calcLiquidationVolume(owner, opt, tkAddr, fd, written);
387          value = calcLiquidationValue(opt, fd.lowerVol, written, volume, iv)
388              .div(_volumeBase);
389  
390          uint now = settings.exchangeTime();
391          bool nearMat = (opt.maturity > now) ? (uint256(opt.maturity).sub(now) < (60 * 60 * 24)) : true;
392  
393          if ((writerCollateralCall[owner][tkAddr] == 0) && (nearMat == false)) {
394              // the first time triggers a margin call event for the owner (how to incentivize? 10$ in exchange credit)
395              if (msg.sender != owner) {
396                  writerCollateralCall[owner][tkAddr] = now;
397                  creditProvider.processIncentivizationPayment(msg.sender, settings.getBaseIncentivisation());
398                  emit CollateralCall(tkAddr, msg.sender, owner, volume);
399              }
400          } else {
401              if ((writerCollateralCall[owner][tkAddr] == 0) && (nearMat == true)) {
402                  //pass, liq right away
403              } else {
404                  require(now.sub(writerCollateralCall[owner][tkAddr]) >= collateralCallPeriod, "Collateral Manager: active collateral call");
405              }
406  
407              if (msg.sender != owner){
408                  // second step triggers the actual liquidation (incentivized, 5% of collateral liquidated in exchange creditbalance, owner gets charged 105%)
409                  uint256 creditingValue = value.mul(5).div(100);
410                  creditProvider.processPayment(owner, tkAddr, value.add(creditingValue));
411                  creditProvider.processIncentivizationPayment(address(settings), creditingValue);
412                  creditProvider.processIncentivizationPayment(msg.sender, settings.getBaseIncentivisation());
413  
414  
415                  if ((collateralSkew() <= 0) && (collateralSkew(opt.udlFeed) > 0)) {
416                      //swap underlying debt for stablecoin debt
417                      debtSwapInternal(opt.udlFeed, creditingValue);
418                  }
419              }
420  
421              if (volume > 0) {
422                  exchange.burn(owner, volume, address(tk));
423              }
424  
425              emit LiquidateEarly(tkAddr, msg.sender, owner, volume);
426          }        
427      }
428  
429      function calcCollateral(address owner, bool is_regular) override public view returns (uint) {     
430          // takes custom collateral requirements and applies exchange level normalizations   
431          int coll = calcCollateralInternal(owner, is_regular);
432          return baseCalcCollateral(coll, is_regular);
433      }
434  
435      function calcNetCollateral(address[] memory _tokens, uint[] memory _uncovered, uint[] memory _holding, bool is_regular) override public view returns (uint) {     
436          // takes custom collateral requirements and applies exchange level normalizations on prospective positions
437          int coll = calcNetCollateralInternal(_tokens, _uncovered, _holding, is_regular);
438          return baseCalcCollateral(coll, is_regular);
439      }
440  
441      function baseCalcCollateral(int coll, bool is_regular) private view returns (uint) {
442          coll = collateralSkewForPosition(coll);
443          coll = coll.div(int(_volumeBase));
444  
445          if (is_regular == false) {
446              return uint(coll);
447          }
448  
449          if (coll < 0)
450              return 0;
451          return uint(coll);
452      }
453  
454      function debtSwap(address udlFeed, uint256 creditingValue) override external {
455          require(msg.sender == address(settings), "non settings");
456          debtSwapInternal(udlFeed,creditingValue);
457      }
458  
459      function debtSwapInternal(address udlFeed, uint256 creditingValue) private {
460          (, address _stablecoin) = settings.getSwapRouterInfo();
461          (, int p) = UnderlyingFeed(udlFeed).getLatestPrice();
462          address underlying = UnderlyingFeed(udlFeed).getUnderlyingAddr();
463  
464          address udlCdtp = vault.getUnderlyingCreditProvider(underlying);
465          uint256 totalUnderlyingBalance = IUnderlyingCreditProvider(udlCdtp).totalTokenStock(); // underlying token balance
466          uint256 totalUnderlyingCreditBalance = IUnderlyingCreditProvider(udlCdtp).getTotalBalance(); // credit balance
467  
468          //NEED TO DO THIS BEFORE CALLING `swapStablecoinForUnderlying` in order to make sure enough stables in udlCdtp
469          address[] memory tokensInOrder = new address[](1);
470          uint[] memory amountsOutInOrder = new uint[](1);
471          tokensInOrder[0] = _stablecoin;
472          amountsOutInOrder[0] = Convert.from18DecimalsBase(_stablecoin, creditingValue);
473          creditProvider.grantTokens(address(udlCdtp), creditingValue, tokensInOrder, amountsOutInOrder);
474  
475          IUnderlyingCreditProvider(udlCdtp).swapStablecoinForUnderlying(
476              udlCdtp,
477              settings.getSwapPath(
478                  _stablecoin,
479                  underlying
480              ),
481              p,
482              creditingValue, //how much stables we are willing to swap for underlying
483              totalUnderlyingCreditBalance.sub(totalUnderlyingBalance)//how much we are short underlying
484          );
485  
486          emit DebtSwap(_stablecoin, underlying, creditingValue);
487      }
488  
489      function daysToMaturity(IOptionsExchange.OptionData memory opt) private view returns (uint d) {
490          uint _now = getUdlNow(opt);
491          if (opt.maturity > _now) {
492              d = (timeBase.mul(uint(opt.maturity).sub(uint(_now)))).div(1 days);
493          } else {
494              d = 0;
495          }
496      }
497  
498      function getUdlPrice(IOptionsExchange.OptionData memory opt) internal view returns (int answer) {
499  
500          if (opt.maturity > settings.exchangeTime()) {
501              (,answer) = UnderlyingFeed(opt.udlFeed).getLatestPrice();
502          } else {
503              (,answer) = UnderlyingFeed(opt.udlFeed).getPrice(opt.maturity);
504          }
505      }
506  
507      function getUdlNow(IOptionsExchange.OptionData memory opt) private view returns (uint timestamp) {
508          (timestamp,) = UnderlyingFeed(opt.udlFeed).getLatestPrice();
509      }
510  }