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 }