diff --git a/TODO b/TODO index 1e78d77c..165a42f3 100644 --- a/TODO +++ b/TODO @@ -1,10 +1,5 @@ Overrides: - * liquidations: will incorrectly calculate liquidatable amount - * verify adding fields to the end of LiquidityStatus doesn't break any ABIs - * view can't correctly query collateral/liability values of individual assets - ? fix this in riskManager.computeAssetLiquidities - * make sure user isn't entered into any other markets (with 0 balance) - * attacker could dust their account and force-disable the override + ? how is BF=0 handled in liquidations Lending logic: g when a token has < 18 decimal places, and a user withdraws their full EToken balance, 0 out the remaining dust so user gets a storage refund diff --git a/addresses/euler-addresses-mainnet.json b/addresses/euler-addresses-mainnet.json index bf471060..5368de10 100644 --- a/addresses/euler-addresses-mainnet.json +++ b/addresses/euler-addresses-mainnet.json @@ -38,6 +38,7 @@ "SwapHandlerUniswapV3": "0x7527E082300fb8D189B3c07dB3BEcc990B5037E7" }, "eulerGeneralView": "0xACC25c4d40651676FEEd43a3467F3169e3E68e42", + "eulerLensV1": "0xACC25c4d40651676FEEd43a3467F3169e3E68e42", "eulerSimpleLens": "0x5077B7642abF198b4a5b7C4BdCE4f03016C7089C", "euler": "0x27182842E098f60e3D576794A5bFFb0777E025d3", "installer": "0x055DE1CCbCC9Bc5291569a0b6aFFdF8b5707aB16", diff --git a/contracts/BaseLogic.sol b/contracts/BaseLogic.sol index f586ae32..49903c6f 100644 --- a/contracts/BaseLogic.sol +++ b/contracts/BaseLogic.sol @@ -574,12 +574,13 @@ abstract contract BaseLogic is BaseModule { return abi.decode(result, (uint)); } - function getAccountLiquidity(address account) internal returns (uint collateralValue, uint liabilityValue) { + function getAccountLiquidity(address account) internal returns (uint collateralValue, uint liabilityValue, uint overrideCollateralValue) { bytes memory result = callInternalModule(MODULEID__RISK_MANAGER, abi.encodeWithSelector(IRiskManager.computeLiquidity.selector, account)); (IRiskManager.LiquidityStatus memory status) = abi.decode(result, (IRiskManager.LiquidityStatus)); collateralValue = status.collateralValue; liabilityValue = status.liabilityValue; + overrideCollateralValue = status.overrideCollateralValue; } function checkLiquidity(address account) internal { @@ -603,7 +604,7 @@ abstract contract BaseLogic is BaseModule { uint currAverageLiquidity; { - (uint collateralValue, uint liabilityValue) = getAccountLiquidity(account); + (uint collateralValue, uint liabilityValue,) = getAccountLiquidity(account); currAverageLiquidity = collateralValue > liabilityValue ? collateralValue - liabilityValue : 0; } diff --git a/contracts/IRiskManager.sol b/contracts/IRiskManager.sol index 6ab4afe6..b5c9cc17 100644 --- a/contracts/IRiskManager.sol +++ b/contracts/IRiskManager.sol @@ -19,8 +19,7 @@ interface IRiskManager { uint liabilityValue; uint numBorrows; bool borrowIsolated; - uint numCollaterals; - bool overrideEnabled; + uint overrideCollateralValue; } struct AssetLiquidity { diff --git a/contracts/Storage.sol b/contracts/Storage.sol index e7de55af..2783c7a4 100644 --- a/contracts/Storage.sol +++ b/contracts/Storage.sol @@ -100,4 +100,6 @@ abstract contract Storage is Constants { } mapping(address => mapping(address => OverrideConfig)) internal overrideLookup; // liability => collateral => OverrideConfig + mapping(address => address[]) internal overrideCollaterals; // liability => collaterals + mapping(address => address[]) internal overrideLiabilities; // collateral => liabilities } diff --git a/contracts/modules/EToken.sol b/contracts/modules/EToken.sol index 8ab8d85a..333e1173 100644 --- a/contracts/modules/EToken.sol +++ b/contracts/modules/EToken.sol @@ -167,10 +167,6 @@ contract EToken is BaseLogic { increaseBalance(assetStorage, assetCache, proxyAddr, account, amountInternal); - // Depositing a token to an account with pre-existing debt in that token creates a self-collateralized loan - // which may result in borrow isolation violation if other tokens are also borrowed on the account - if (assetStorage.users[account].owed != 0) checkLiquidity(account); - logAssetStatus(assetCache); } @@ -344,10 +340,6 @@ contract EToken is BaseLogic { checkLiquidity(from); - // Depositing a token to an account with pre-existing debt in that token creates a self-collateralized loan - // which may result in borrow isolation violation if other tokens are also borrowed on the account - if (assetStorage.users[to].owed != 0) checkLiquidity(to); - logAssetStatus(assetCache); return true; diff --git a/contracts/modules/Exec.sol b/contracts/modules/Exec.sol index dbc3a40e..1e6ccc8a 100644 --- a/contracts/modules/Exec.sol +++ b/contracts/modules/Exec.sol @@ -50,7 +50,7 @@ contract Exec is BaseLogic { /// @notice Compute detailed liquidity for an account, broken down by asset /// @param account User address /// @return assets List of user's entered assets and each asset's corresponding liquidity - function detailedLiquidity(address account) public staticDelegate returns (IRiskManager.AssetLiquidity[] memory assets) { + function liquidityPerAsset(address account) public staticDelegate returns (IRiskManager.AssetLiquidity[] memory assets) { bytes memory result = callInternalModule(MODULEID__RISK_MANAGER, abi.encodeWithSelector(IRiskManager.computeAssetLiquidities.selector, account)); @@ -321,4 +321,45 @@ contract Exec is BaseLogic { if (status == DEFERLIQUIDITY__DIRTY) checkLiquidity(account); } } + + + + + // Deprecated functions for backward compatibility. May be removed in the future. + + struct LegacyLiquidityStatus { + uint collateralValue; + uint liabilityValue; + uint numBorrows; + bool borrowIsolated; + } + struct LegacyAssetLiquidity { + address underlying; + LegacyLiquidityStatus status; + } + + // DEPRECATED. Use liquidityPerAsset instead. + function detailedLiquidity(address account) public staticDelegate returns (LegacyAssetLiquidity[] memory) { + bytes memory result = callInternalModule(MODULEID__RISK_MANAGER, + abi.encodeWithSelector(IRiskManager.computeAssetLiquidities.selector, account)); + + (IRiskManager.AssetLiquidity[] memory assetLiquidities) = abi.decode(result, (IRiskManager.AssetLiquidity[])); + + LegacyAssetLiquidity[] memory assets = new LegacyAssetLiquidity[](assetLiquidities.length); + + for (uint i = 0; i < assetLiquidities.length; ++i) { + IRiskManager.LiquidityStatus memory status = assetLiquidities[i].status; + assets[i] = LegacyAssetLiquidity({ + underlying: assetLiquidities[i].underlying, + status: LegacyLiquidityStatus({ + collateralValue: status.collateralValue, + liabilityValue: status.liabilityValue, + numBorrows: status.numBorrows, + borrowIsolated: status.borrowIsolated + }) + }); + } + + return assets; + } } diff --git a/contracts/modules/Governance.sol b/contracts/modules/Governance.sol index e9e8d9cf..ee3a5d4c 100644 --- a/contracts/modules/Governance.sol +++ b/contracts/modules/Governance.sol @@ -98,10 +98,6 @@ contract Governance is BaseLogic { increaseBalance(assetStorage, assetCache, eTokenAddress, recipient, amount); - // Depositing a token to an account with pre-existing debt in that token creates a self-collateralized loan - // which may result in borrow isolation violation if other tokens are also borrowed on the account - if (assetStorage.users[recipient].owed != 0) checkLiquidity(recipient); - logAssetStatus(assetCache); emit GovConvertReserves(underlying, recipient, balanceToUnderlyingAmount(assetCache, amount)); @@ -118,14 +114,34 @@ contract Governance is BaseLogic { } function setOverride(address liability, address collateral, OverrideConfig calldata newOverride) external nonReentrant governorOnly { - require(underlyingLookup[liability].eTokenAddress != address(0), "e/gov/liability-not-activated"); - require(underlyingLookup[collateral].eTokenAddress != address(0), "e/gov/collateral-not-activated"); - overrideLookup[liability][collateral] = newOverride; + updateOverridesArray(overrideCollaterals[liability], collateral, newOverride); + updateOverridesArray(overrideLiabilities[collateral], liability, newOverride); + emit GovSetOverride(liability, collateral, newOverride); } + function updateOverridesArray(address[] storage arr, address asset, OverrideConfig calldata newOverride) private { + uint length = arr.length; + if (newOverride.enabled) { + for (uint i = 0; i < length;) { + if (arr[i] == asset) return; + unchecked { ++i; } + } + arr.push(asset); + } else { + for (uint i = 0; i < length;) { + if (arr[i] == asset) { + arr[i] = arr[length - 1]; + arr.pop(); + return; + } + unchecked { ++i; } + } + } + } + // getters function getGovernorAdmin() external view returns (address) { diff --git a/contracts/modules/Liquidation.sol b/contracts/modules/Liquidation.sol index 72d7dec4..f1478e65 100644 --- a/contracts/modules/Liquidation.sol +++ b/contracts/modules/Liquidation.sol @@ -47,6 +47,13 @@ contract Liquidation is BaseLogic { uint underlyingPrice; uint collateralPrice; + uint collateralValue; + uint overrideCollateralValue; + uint liabilityValue; + + uint currentOwed; + uint collateralBalance; + LiquidationOpportunity liqOpp; uint repayPreFees; @@ -60,26 +67,35 @@ contract Liquidation is BaseLogic { liqLocs.underlyingPrice = getAssetPrice(liqLocs.underlying); liqLocs.collateralPrice = getAssetPrice(liqLocs.collateral); - LiquidationOpportunity memory liqOpp = liqLocs.liqOpp; + { + AssetStorage storage underlyingAssetStorage = eTokenLookup[underlyingLookup[liqLocs.underlying].eTokenAddress]; + AssetCache memory underlyingAssetCache = loadAssetCache(liqLocs.underlying, underlyingAssetStorage); + liqLocs.currentOwed = getCurrentOwed(underlyingAssetStorage, underlyingAssetCache, liqLocs.violator); - AssetStorage storage underlyingAssetStorage = eTokenLookup[underlyingLookup[liqLocs.underlying].eTokenAddress]; - AssetCache memory underlyingAssetCache = loadAssetCache(liqLocs.underlying, underlyingAssetStorage); - AssetStorage storage collateralAssetStorage = eTokenLookup[underlyingLookup[liqLocs.collateral].eTokenAddress]; - AssetCache memory collateralAssetCache = loadAssetCache(liqLocs.collateral, collateralAssetStorage); + AssetStorage storage collateralAssetStorage = eTokenLookup[underlyingLookup[liqLocs.collateral].eTokenAddress]; + AssetCache memory collateralAssetCache = loadAssetCache(liqLocs.collateral, collateralAssetStorage); + liqLocs.collateralBalance = balanceToUnderlyingAmount(collateralAssetCache, collateralAssetStorage.users[liqLocs.violator].balance); + + (uint collateralValue, uint liabilityValue, uint overrideCollateralValue) = getAccountLiquidity(liqLocs.violator); + liqLocs.collateralValue = collateralValue; + liqLocs.liabilityValue = liabilityValue; + liqLocs.overrideCollateralValue = overrideCollateralValue; + } + + LiquidationOpportunity memory liqOpp = liqLocs.liqOpp; liqOpp.repay = liqOpp.yield = 0; - (uint collateralValue, uint liabilityValue) = getAccountLiquidity(liqLocs.violator); - if (liabilityValue == 0) { + if (liqLocs.liabilityValue == 0) { liqOpp.healthScore = type(uint).max; return; // no violation } - liqOpp.healthScore = collateralValue * 1e18 / liabilityValue; + liqOpp.healthScore = liqLocs.collateralValue * 1e18 / liqLocs.liabilityValue; - if (collateralValue >= liabilityValue) { + if (liqLocs.collateralValue >= liqLocs.liabilityValue) { return; // no violation } @@ -90,7 +106,7 @@ contract Liquidation is BaseLogic { { uint baseDiscount = UNDERLYING_RESERVES_FEE + (1e18 - liqOpp.healthScore); - uint discountBooster = computeDiscountBooster(liqLocs.liquidator, liabilityValue); + uint discountBooster = computeDiscountBooster(liqLocs.liquidator, liqLocs.liabilityValue); uint discount = baseDiscount * discountBooster / 1e18; @@ -99,41 +115,138 @@ contract Liquidation is BaseLogic { liqOpp.baseDiscount = baseDiscount; liqOpp.discount = discount; + liqOpp.conversionRate = liqLocs.underlyingPrice * 1e18 / liqLocs.collateralPrice * 1e18 / (1e18 - discount); } // Determine amount to repay to bring user to target health - if (liqLocs.underlying == liqLocs.collateral) { + OverrideConfig memory overrideConfig; + AssetConfig memory collateralConfig; + AssetConfig memory underlyingConfig; + + collateralConfig = resolveAssetConfig(liqLocs.collateral); + underlyingConfig = resolveAssetConfig(liqLocs.underlying); + + uint collateralFactor = collateralConfig.collateralFactor; + uint borrowFactor = underlyingConfig.borrowFactor; + + if (borrowFactor == 0) { + // Arbitrarily liquidate full collateral liqOpp.repay = type(uint).max; - } else { - AssetConfig memory collateralConfig = resolveAssetConfig(liqLocs.collateral); - AssetConfig memory underlyingConfig = resolveAssetConfig(liqLocs.underlying); + boundRepayAndYield(liqOpp, liqLocs); + addReserveFee(liqOpp, liqLocs); + return; + } + + // If override is active for the liquidated pair, assume the resulting liability will be fully covered by override collateral, and adjust inputs + if (liqLocs.overrideCollateralValue > 0) { + overrideConfig = overrideLookup[liqLocs.underlying][liqLocs.collateral]; + // self-collateralization is an implicit override + if (!overrideConfig.enabled && liqLocs.underlying == liqLocs.collateral) { + overrideConfig.enabled = true; + overrideConfig.collateralFactor = SELF_COLLATERAL_FACTOR; + } + // the liquidated collateral has active override with liability + if (overrideConfig.enabled) { + collateralFactor = overrideConfig.collateralFactor; + borrowFactor = CONFIG_FACTOR_SCALE; + // adjust the whole liability for override BF = 1 + liqLocs.liabilityValue = liqLocs.currentOwed * liqLocs.underlyingPrice / 1e18; + } + } + + // Calculate for no overrides or all overrides or if both override and regular collateral are present : + // - assume resulting liability fully covered by override if liquidating override collateral + // - assume resulting liability partially covered by override if liquidating regular collateral + calculateRepayCommon(liqOpp, liqLocs, collateralFactor, borrowFactor); + // Limit repay and yield to current debt and available collateral + boundRepayAndYield(liqOpp, liqLocs); + + // If in override, test the assumptions and adjust if needed + if (liqLocs.overrideCollateralValue > 0) { + // Correction when liquidating override collateral + if ( + // override and regular collateral present + liqLocs.overrideCollateralValue != liqLocs.collateralValue && + // liquidating collateral with override + overrideConfig.enabled && + // not already maxed out + liqOpp.yield != liqLocs.collateralBalance && + // result is not fully covered by override collateral as expected + (liqOpp.repay == 0 || // numerator in equation was negative + (liqLocs.currentOwed - liqOpp.repay) * liqLocs.underlyingPrice / 1e18 > + liqLocs.overrideCollateralValue - liqOpp.yield * collateralFactor / CONFIG_FACTOR_SCALE * liqLocs.collateralPrice / 1e18) + ) { + borrowFactor = underlyingConfig.borrowFactor; + + uint auxAdj = 1e18 * CONFIG_FACTOR_SCALE / borrowFactor - 1e18; + uint borrowAdj = TARGET_HEALTH * CONFIG_FACTOR_SCALE / borrowFactor; + uint collateralAdj = 1e18 * collateralFactor / CONFIG_FACTOR_SCALE * (TARGET_HEALTH * auxAdj / 1e18 + 1e18) / (1e18 - liqOpp.discount); + + uint overrideCollateralValueAdj = liqLocs.overrideCollateralValue * auxAdj / 1e18; + uint liabilityValueAdj = liqLocs.currentOwed * liqLocs.underlyingPrice / 1e18 * CONFIG_FACTOR_SCALE / borrowFactor; + + if (liabilityValueAdj <= overrideCollateralValueAdj || borrowAdj <= collateralAdj) { + liqOpp.repay = type(uint).max; + } else { + liabilityValueAdj = liabilityValueAdj - overrideCollateralValueAdj; + liabilityValueAdj = TARGET_HEALTH * liabilityValueAdj / 1e18; + + liqOpp.repay = liabilityValueAdj > liqLocs.collateralValue + ? (liabilityValueAdj - liqLocs.collateralValue) * 1e18 / (borrowAdj - collateralAdj) * 1e18 / liqLocs.underlyingPrice + : type(uint).max; + } + + boundRepayAndYield(liqOpp, liqLocs); + } - uint collateralFactor = collateralConfig.collateralFactor; - uint borrowFactor = underlyingConfig.borrowFactor; + // Correction when liquidating regular collateral with overrides present + if ( + // liquidating regular collateral + !overrideConfig.enabled && + // not already maxed out + liqOpp.yield != liqLocs.collateralBalance && + // result is not partially collateralised as expected + (liqLocs.currentOwed - liqOpp.repay) * liqLocs.underlyingPrice / 1e18 < liqLocs.overrideCollateralValue + ) { + // adjust the whole liability for override BF = 1 + liqLocs.liabilityValue = liqLocs.currentOwed * liqLocs.underlyingPrice / 1e18; - uint liabilityValueTarget = liabilityValue * TARGET_HEALTH / 1e18; + collateralFactor = collateralConfig.collateralFactor; + calculateRepayCommon(liqOpp, liqLocs, collateralFactor, CONFIG_FACTOR_SCALE); - // These factors are first converted into standard 1e18-scale fractions, then adjusted according to TARGET_HEALTH and the discount: - uint borrowAdj = borrowFactor != 0 ? TARGET_HEALTH * CONFIG_FACTOR_SCALE / borrowFactor : MAX_SANE_DEBT_AMOUNT; - uint collateralAdj = 1e18 * uint(collateralFactor) / CONFIG_FACTOR_SCALE * 1e18 / (1e18 - liqOpp.discount); + liqOpp.repay = liqOpp.repay == 0 ? type(uint).max : liqOpp.repay; + boundRepayAndYield(liqOpp, liqLocs); + } + } + + addReserveFee(liqOpp, liqLocs); + } + + function calculateRepayCommon(LiquidationOpportunity memory liqOpp, LiquidationLocals memory liqLocs, uint collateralFactor, uint borrowFactor) private pure { + // These factors are first converted into standard 1e18-scale fractions, then adjusted according to TARGET_HEALTH and the discount: + uint borrowAdj = borrowFactor != 0 ? TARGET_HEALTH * CONFIG_FACTOR_SCALE / borrowFactor : MAX_SANE_DEBT_AMOUNT; + uint collateralAdj = 1e18 * collateralFactor / CONFIG_FACTOR_SCALE * 1e18 / (1e18 - liqOpp.discount); + uint liabilityValue = liqLocs.liabilityValue * TARGET_HEALTH / 1e18; + + if (liabilityValue > liqLocs.collateralValue) { if (borrowAdj <= collateralAdj) { liqOpp.repay = type(uint).max; } else { - // liabilityValueTarget >= liabilityValue > collateralValue - uint maxRepayInReference = (liabilityValueTarget - collateralValue) * 1e18 / (borrowAdj - collateralAdj); + uint maxRepayInReference = (liabilityValue - liqLocs.collateralValue) * 1e18 / (borrowAdj - collateralAdj); liqOpp.repay = maxRepayInReference * 1e18 / liqLocs.underlyingPrice; } } + } + function boundRepayAndYield(LiquidationOpportunity memory liqOpp, LiquidationLocals memory liqLocs) private pure { // Limit repay to current owed // This can happen when there are multiple borrows and liquidating this one won't bring the violator back to solvency - { - uint currentOwed = getCurrentOwed(underlyingAssetStorage, underlyingAssetCache, liqLocs.violator); - if (liqOpp.repay > currentOwed) liqOpp.repay = currentOwed; + if (liqOpp.repay > liqLocs.currentOwed) { + liqOpp.repay = liqLocs.currentOwed; } // Limit yield to borrower's available collateral, and reduce repay if necessary @@ -141,17 +254,14 @@ contract Liquidation is BaseLogic { liqOpp.yield = liqOpp.repay * liqOpp.conversionRate / 1e18; - { - uint collateralBalance = balanceToUnderlyingAmount(collateralAssetCache, collateralAssetStorage.users[liqLocs.violator].balance); - - if (collateralBalance < liqOpp.yield) { - liqOpp.repay = collateralBalance * 1e18 / liqOpp.conversionRate; - liqOpp.yield = collateralBalance; - } + if (liqLocs.collateralBalance < liqOpp.yield) { + liqOpp.repay = liqLocs.collateralBalance * 1e18 / liqOpp.conversionRate; + liqOpp.yield = liqLocs.collateralBalance; } + } + function addReserveFee(LiquidationOpportunity memory liqOpp, LiquidationLocals memory liqLocs) private pure { // Adjust repay to account for reserves fee - liqLocs.repayPreFees = liqOpp.repay; liqOpp.repay = liqOpp.repay * (1e18 + UNDERLYING_RESERVES_FEE) / 1e18; } diff --git a/contracts/modules/Markets.sol b/contracts/modules/Markets.sol index b0bd366c..c5557a73 100644 --- a/contracts/modules/Markets.sol +++ b/contracts/modules/Markets.sol @@ -279,4 +279,32 @@ contract Markets is BaseLogic { checkLiquidity(account); } } + + // Overrides + + /// @notice Retrieves collateral factor override for asset pair + /// @param liability Borrowed underlying + /// @param collateral Collateral underlying + /// @return Override config set for the pair + function getOverride(address liability, address collateral) external view returns (OverrideConfig memory) { + return overrideLookup[liability][collateral]; + } + + /// @notice Retrieves a list of collaterals configured through override for the liability asset + /// @param liability Borrowed underlying + /// @return List of underlying collaterals with override configured + /// @dev The list can have duplicates. Returned assets could have the override disabled + function getOverrideCollaterals(address liability) external view returns (address[] memory) { + return overrideCollaterals[liability]; + } + + /// @notice Retrieves a list of liabilities configured through override for the collateral asset + /// @param collateral Collateral underlying + /// @return List of underlying liabilities with override configured + /// @dev The list can have duplicates. Returned assets could have the override disabled + function getOverrideLiabilities(address collateral) external view returns (address[] memory) { + return overrideLiabilities[collateral]; + } + + } diff --git a/contracts/modules/RiskManager.sol b/contracts/modules/RiskManager.sol index 683e8f24..5ecdacd3 100644 --- a/contracts/modules/RiskManager.sol +++ b/contracts/modules/RiskManager.sol @@ -7,8 +7,6 @@ import "../IRiskManager.sol"; import "../vendor/TickMath.sol"; import "../vendor/FullMath.sol"; - - interface IUniswapV3Factory { function getPool(address tokenA, address tokenB, uint24 fee) external view returns (address pool); } @@ -27,7 +25,6 @@ interface IChainlinkAggregatorV2V3 { contract RiskManager is IRiskManager, BaseLogic { // Construction - address immutable referenceAsset; // Token must have 18 decimals address immutable uniswapFactory; bytes32 immutable uniswapPoolInitCodeHash; @@ -286,120 +283,142 @@ contract RiskManager is IRiskManager, BaseLogic { // Liquidity - - struct OverrideCache { - address liability; - uint liabilityValue; - - address collateral; - uint collateralValue; + struct Cache { + uint assetLiability; + uint borrowFactor; } - function computeLiquidityRaw(address account, address[] memory underlyings) private view returns (LiquidityStatus memory status) { + function computeLiquidityRaw(address account, address[] memory underlyings, address singleLiability) private view returns (LiquidityStatus memory status) { status.collateralValue = 0; status.liabilityValue = 0; status.numBorrows = 0; status.borrowIsolated = false; - status.numCollaterals = 0; - status.overrideEnabled = false; + status.overrideCollateralValue = 0; - OverrideCache memory overrideCache; AssetConfig memory config; AssetStorage storage assetStorage; AssetCache memory assetCache; + Cache memory cache; + for (uint i = 0; i < underlyings.length; ++i) { address underlying = underlyings[i]; - config = resolveAssetConfig(underlying); assetStorage = eTokenLookup[config.eTokenAddress]; - uint balance = assetStorage.users[account].balance; - uint owed = assetStorage.users[account].owed; - if (owed != 0) { - initAssetCache(underlying, assetStorage, assetCache); - (uint price,) = getPriceInternal(assetCache, config); + if (assetStorage.users[account].owed == 0 && balance == 0) continue; - status.numBorrows++; - overrideCache.liability = underlying; + initAssetCache(underlying, assetStorage, assetCache); + (uint price,) = getPriceInternal(assetCache, config); + // Count liability + if (assetStorage.users[account].owed != 0) { + status.numBorrows++; if (config.borrowIsolated) status.borrowIsolated = true; - uint assetLiability = getCurrentOwed(assetStorage, assetCache, account); + if (config.borrowFactor == 0) { + status.liabilityValue = MAX_SANE_DEBT_AMOUNT; + } else { + uint assetLiability = getCurrentOwed(assetStorage, assetCache, account); + assetLiability = assetLiability * price / 1e18; - if (balance != 0) { // self-collateralisation - status.numCollaterals++; - overrideCache.collateral = underlying; + // cache non-risk-adjusted liability value in case override is active + cache.assetLiability = assetLiability; - uint balanceInUnderlying = balanceToUnderlyingAmount(assetCache, balance); + assetLiability = assetLiability * CONFIG_FACTOR_SCALE / config.borrowFactor; + status.liabilityValue += assetLiability; - uint selfAmount = assetLiability; - uint selfAmountAdjusted = assetLiability * CONFIG_FACTOR_SCALE / SELF_COLLATERAL_FACTOR; + // cache borrow factor in case override is active + cache.borrowFactor = config.borrowFactor; + } + } - if (selfAmountAdjusted > balanceInUnderlying) { - selfAmount = balanceInUnderlying * SELF_COLLATERAL_FACTOR / CONFIG_FACTOR_SCALE; - selfAmountAdjusted = balanceInUnderlying; - } + // Count collateral + if (balance != 0) { + OverrideConfig memory overrideConfig; + overrideConfig.enabled = false; - { - uint assetCollateral = (balanceInUnderlying - selfAmountAdjusted) * config.collateralFactor / CONFIG_FACTOR_SCALE; - assetCollateral += selfAmount; - status.collateralValue += assetCollateral * price / 1e18; - } + if (singleLiability != address(0)) { + overrideConfig = overrideLookup[singleLiability][underlying]; - assetLiability -= selfAmount; - status.liabilityValue += selfAmount * price / 1e18; - status.borrowIsolated = true; // self-collateralised loans are always isolated + // self-collateralization is an implicit override + if (!overrideConfig.enabled && singleLiability == underlying) { + overrideConfig.enabled = true; + overrideConfig.collateralFactor = SELF_COLLATERAL_FACTOR; + } } - assetLiability = overrideCache.liabilityValue = assetLiability * price / 1e18; - assetLiability = config.borrowFactor != 0 ? assetLiability * CONFIG_FACTOR_SCALE / config.borrowFactor : MAX_SANE_DEBT_AMOUNT; - status.liabilityValue += assetLiability; - } else if (balance != 0 && config.collateralFactor != 0) { - initAssetCache(underlying, assetStorage, assetCache); - (uint price,) = getPriceInternal(assetCache, config); - - status.numCollaterals++; - overrideCache.collateral = underlying; - - uint balanceInUnderlying = balanceToUnderlyingAmount(assetCache, balance); - uint assetCollateral = overrideCache.collateralValue = balanceInUnderlying * price / 1e18; - assetCollateral = assetCollateral * config.collateralFactor / CONFIG_FACTOR_SCALE; - status.collateralValue += assetCollateral; + if (config.collateralFactor != 0 || overrideConfig.enabled) { + uint balanceInUnderlying = balanceToUnderlyingAmount(assetCache, balance); + uint assetCollateral = balanceInUnderlying * price / 1e18; + if (overrideConfig.enabled) { + status.overrideCollateralValue += assetCollateral * overrideConfig.collateralFactor / CONFIG_FACTOR_SCALE; + } else { + status.collateralValue += assetCollateral * config.collateralFactor / CONFIG_FACTOR_SCALE; + } + } } } - if (status.numBorrows == 1 && status.numCollaterals == 1 && overrideCache.liability != overrideCache.collateral) { - OverrideConfig memory overrideConfig = overrideLookup[overrideCache.liability][overrideCache.collateral]; - - if (overrideConfig.enabled) { - status.overrideEnabled = true; - status.collateralValue = overrideCache.collateralValue * overrideConfig.collateralFactor / CONFIG_FACTOR_SCALE; - status.liabilityValue = overrideCache.liabilityValue; + // Adjust collateral and liability value if in override + if (status.overrideCollateralValue > 0) { + if (status.liabilityValue < MAX_SANE_DEBT_AMOUNT) { + // liability covered by override is counted with borrow factor 1, the rest with regular borrow factor + status.liabilityValue = cache.assetLiability; + status.liabilityValue = status.overrideCollateralValue < status.liabilityValue + ? status.overrideCollateralValue + (status.liabilityValue - status.overrideCollateralValue) * CONFIG_FACTOR_SCALE / cache.borrowFactor + : status.liabilityValue; } + + status.collateralValue += status.overrideCollateralValue; } } function computeLiquidity(address account) public view override returns (LiquidityStatus memory) { - return computeLiquidityRaw(account, getEnteredMarketsArray(account)); + address[] memory underlyings = getEnteredMarketsArray(account); + address singleLiability = findSingleLiability(account, underlyings); + return computeLiquidityRaw(account, underlyings, singleLiability); } function computeAssetLiquidities(address account) external view override returns (AssetLiquidity[] memory) { address[] memory underlyings = getEnteredMarketsArray(account); + address singleLiability = findSingleLiability(account, underlyings); AssetLiquidity[] memory output = new AssetLiquidity[](underlyings.length); address[] memory singleUnderlying = new address[](1); - for (uint i = 0; i < underlyings.length; ++i) { + for (uint i = 0; i < underlyings.length;) { output[i].underlying = singleUnderlying[0] = underlyings[i]; - output[i].status = computeLiquidityRaw(account, singleUnderlying); + output[i].status = computeLiquidityRaw(account, singleUnderlying, singleLiability); + + // the override liability is only possible to calculate with all underlyings + if (singleLiability == underlyings[i]) { + LiquidityStatus memory status = computeLiquidityRaw(account, underlyings, singleLiability); + output[i].status.liabilityValue = status.liabilityValue; + } + + unchecked { ++i; } } return output; } + function findSingleLiability(address account, address[] memory underlyings) private view returns (address singleLiabilityAddress) { + for (uint i = 0; i < underlyings.length; ++i) { + address underlying = underlyings[i]; + if (eTokenLookup[underlyingLookup[underlying].eTokenAddress].users[account].owed > 0) { + if (singleLiabilityAddress == address(0)) { + singleLiabilityAddress = underlying; + } else { + singleLiabilityAddress = address(0); + break; + } + } + } + } + function requireLiquidity(address account) external view override { LiquidityStatus memory status = computeLiquidity(account); diff --git a/contracts/modules/Swap.sol b/contracts/modules/Swap.sol index 63f6240b..99b3ef09 100644 --- a/contracts/modules/Swap.sol +++ b/contracts/modules/Swap.sol @@ -429,10 +429,6 @@ contract Swap is BaseLogic { increaseBalance(assetStorage, assetCache, eTokenAddress, account, amountInternal); - // Depositing a token to an account with pre-existing debt in that token creates a self-collateralized loan - // which may result in borrow isolation violation if other tokens are also borrowed on the account - if (assetStorage.users[account].owed != 0) checkLiquidity(account); - logAssetStatus(assetCache); } diff --git a/contracts/modules/SwapHub.sol b/contracts/modules/SwapHub.sol index eb301c77..e65afd5b 100644 --- a/contracts/modules/SwapHub.sol +++ b/contracts/modules/SwapHub.sol @@ -64,11 +64,6 @@ contract SwapHub is BaseLogic { // Check liquidity checkLiquidity(cache.accountIn); - - // Depositing a token to the account with a pre-existing debt in that token creates a self-collateralized loan - // which may result in borrow isolation violation if other tokens are also borrowed on the account - if (cache.accountIn != cache.accountOut && assetStorageOut.users[cache.accountOut].owed != 0) - checkLiquidity(cache.accountOut); } /// @notice Repay debt by selling another deposited token diff --git a/contracts/views/EulerGeneralView.sol b/contracts/views/EulerGeneralView.sol index 1cf4d1fd..40cc2cb4 100644 --- a/contracts/views/EulerGeneralView.sol +++ b/contracts/views/EulerGeneralView.sol @@ -16,6 +16,10 @@ interface IExec { function liquidity(address account) external view returns (IRiskManager.LiquidityStatus memory status); } + +// DEPRECATED. Use EulerLensV1 instead. + + contract EulerGeneralView is Constants { bytes32 immutable public moduleGitCommit; diff --git a/contracts/views/EulerLensV1.sol b/contracts/views/EulerLensV1.sol new file mode 100644 index 00000000..88e50c66 --- /dev/null +++ b/contracts/views/EulerLensV1.sol @@ -0,0 +1,291 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.0; + +import "../Euler.sol"; +import "../Storage.sol"; +import "../modules/EToken.sol"; +import "../modules/Markets.sol"; +import "../BaseIRMLinearKink.sol"; +import "../vendor/RPow.sol"; + +interface IExec { + function getPriceFull(address underlying) external view returns (uint twap, uint twapPeriod, uint currPrice); + function getPrice(address underlying) external view returns (uint twap, uint twapPeriod); + function liquidityPerAsset(address account) external view returns (IRiskManager.AssetLiquidity[] memory assets); + function liquidity(address account) external view returns (IRiskManager.LiquidityStatus memory status); +} + +contract EulerLensV1 is Constants { + bytes32 immutable public moduleGitCommit; + + constructor(bytes32 moduleGitCommit_) { + moduleGitCommit = moduleGitCommit_; + } + + // Query + + struct Query { + address eulerContract; + + address account; + address[] markets; + } + + // Response + + struct Override { + address underlying; + uint32 collateralFactor; + } + + struct ResponseMarket { + // Universal + + address underlying; + string name; + string symbol; + uint8 decimals; + + address eTokenAddr; + address dTokenAddr; + address pTokenAddr; + + Storage.AssetConfig config; + + uint poolSize; + uint totalBalances; + uint totalBorrows; + uint reserveBalance; + + uint32 reserveFee; + uint borrowAPY; + uint supplyAPY; + + // Pricing + + uint twap; + uint twapPeriod; + uint currPrice; + uint16 pricingType; + uint32 pricingParameters; + address pricingForwarded; + + // Account specific + + uint underlyingBalance; + uint eulerAllowance; + uint eTokenBalance; + uint eTokenBalanceUnderlying; + uint dTokenBalance; + IRiskManager.LiquidityStatus liquidityStatus; + + // Overrides + + Override[] overrideLiabilities; + Override[] overrideCollaterals; + } + + struct Response { + uint timestamp; + uint blockNumber; + + ResponseMarket[] markets; + address[] enteredMarkets; + } + + + + // Implementation + + function doQueryBatch(Query[] memory qs) external view returns (Response[] memory r) { + r = new Response[](qs.length); + + for (uint i = 0; i < qs.length; ++i) { + r[i] = doQuery(qs[i]); + } + } + + function doQuery(Query memory q) public view returns (Response memory r) { + r.timestamp = block.timestamp; + r.blockNumber = block.number; + + Euler eulerProxy = Euler(q.eulerContract); + + Markets marketsProxy = Markets(eulerProxy.moduleIdToProxy(MODULEID__MARKETS)); + IExec execProxy = IExec(eulerProxy.moduleIdToProxy(MODULEID__EXEC)); + + IRiskManager.AssetLiquidity[] memory liqs; + + if (q.account != address(0)) { + liqs = execProxy.liquidityPerAsset(q.account); + } + + r.markets = new ResponseMarket[](liqs.length + q.markets.length); + + for (uint i = 0; i < liqs.length; ++i) { + ResponseMarket memory m = r.markets[i]; + + m.underlying = liqs[i].underlying; + m.liquidityStatus = liqs[i].status; + + populateResponseMarket(q, m, marketsProxy, execProxy); + } + + for (uint j = liqs.length; j < liqs.length + q.markets.length; ++j) { + uint i = j - liqs.length; + ResponseMarket memory m = r.markets[j]; + + m.underlying = q.markets[i]; + + populateResponseMarket(q, m, marketsProxy, execProxy); + } + + if (q.account != address(0)) { + r.enteredMarkets = marketsProxy.getEnteredMarkets(q.account); + } + } + + function populateResponseMarket(Query memory q, ResponseMarket memory m, Markets marketsProxy, IExec execProxy) private view { + m.name = getStringOrBytes32(m.underlying, IERC20.name.selector); + m.symbol = getStringOrBytes32(m.underlying, IERC20.symbol.selector); + + m.decimals = IERC20(m.underlying).decimals(); + + m.eTokenAddr = marketsProxy.underlyingToEToken(m.underlying); + if (m.eTokenAddr == address(0)) return; // not activated + + m.dTokenAddr = marketsProxy.eTokenToDToken(m.eTokenAddr); + m.pTokenAddr = marketsProxy.underlyingToPToken(m.underlying); + + { + Storage.AssetConfig memory c = marketsProxy.underlyingToAssetConfig(m.underlying); + m.config = c; + } + + m.poolSize = IERC20(m.underlying).balanceOf(q.eulerContract); + m.totalBalances = EToken(m.eTokenAddr).totalSupplyUnderlying(); + m.totalBorrows = IERC20(m.dTokenAddr).totalSupply(); + m.reserveBalance = EToken(m.eTokenAddr).reserveBalanceUnderlying(); + + m.reserveFee = marketsProxy.reserveFee(m.underlying); + + { + uint borrowSPY = uint(int(marketsProxy.interestRate(m.underlying))); + (m.borrowAPY, m.supplyAPY) = computeAPYs(borrowSPY, m.totalBorrows, m.totalBalances, m.reserveFee); + } + + (m.twap, m.twapPeriod, m.currPrice) = execProxy.getPriceFull(m.underlying); + (m.pricingType, m.pricingParameters, m.pricingForwarded) = marketsProxy.getPricingConfig(m.underlying); + + if (q.account == address(0)) return; + + m.underlyingBalance = IERC20(m.underlying).balanceOf(q.account); + m.eTokenBalance = IERC20(m.eTokenAddr).balanceOf(q.account); + m.eTokenBalanceUnderlying = EToken(m.eTokenAddr).balanceOfUnderlying(q.account); + m.dTokenBalance = IERC20(m.dTokenAddr).balanceOf(q.account); + m.eulerAllowance = IERC20(m.underlying).allowance(q.account, q.eulerContract); + + { + address[] memory overrideCollaterals = marketsProxy.getOverrideCollaterals(m.underlying); + m.overrideCollaterals = new Override[](overrideCollaterals.length); + for (uint i = 0; i < overrideCollaterals.length; i++) { + m.overrideCollaterals[i] = Override({ + underlying: overrideCollaterals[i], + collateralFactor: marketsProxy.getOverride(m.underlying, overrideCollaterals[i]).collateralFactor + }); + } + + address[] memory overrideLiabilities = marketsProxy.getOverrideLiabilities(m.underlying); + m.overrideLiabilities = new Override[](overrideLiabilities.length); + for (uint i = 0; i < overrideLiabilities.length; i++) { + m.overrideLiabilities[i] = Override({ + underlying: overrideLiabilities[i], + collateralFactor: marketsProxy.getOverride(overrideLiabilities[i], m.underlying).collateralFactor + }); + } + } + } + + + function computeAPYs(uint borrowSPY, uint totalBorrows, uint totalBalancesUnderlying, uint32 reserveFee) public pure returns (uint borrowAPY, uint supplyAPY) { + borrowAPY = RPow.rpow(borrowSPY + 1e27, SECONDS_PER_YEAR, 10**27) - 1e27; + + uint supplySPY = totalBalancesUnderlying == 0 ? 0 : borrowSPY * totalBorrows / totalBalancesUnderlying; + supplySPY = supplySPY * (RESERVE_FEE_SCALE - reserveFee) / RESERVE_FEE_SCALE; + supplyAPY = RPow.rpow(supplySPY + 1e27, SECONDS_PER_YEAR, 10**27) - 1e27; + } + + + + // Interest rate model queries + + struct QueryIRM { + address eulerContract; + address underlying; + } + + struct ResponseIRM { + uint kink; + + uint baseAPY; + uint kinkAPY; + uint maxAPY; + + uint baseSupplyAPY; + uint kinkSupplyAPY; + uint maxSupplyAPY; + } + + function doQueryIRM(QueryIRM memory q) external view returns (ResponseIRM memory r) { + Euler eulerProxy = Euler(q.eulerContract); + Markets marketsProxy = Markets(eulerProxy.moduleIdToProxy(MODULEID__MARKETS)); + + uint moduleId = marketsProxy.interestRateModel(q.underlying); + address moduleImpl = eulerProxy.moduleIdToImplementation(moduleId); + + BaseIRMLinearKink irm = BaseIRMLinearKink(moduleImpl); + + uint kink = r.kink = irm.kink(); + uint32 reserveFee = marketsProxy.reserveFee(q.underlying); + + uint baseSPY = irm.baseRate(); + uint kinkSPY = baseSPY + (kink * irm.slope1()); + uint maxSPY = kinkSPY + ((type(uint32).max - kink) * irm.slope2()); + + (r.baseAPY, r.baseSupplyAPY) = computeAPYs(baseSPY, 0, type(uint32).max, reserveFee); + (r.kinkAPY, r.kinkSupplyAPY) = computeAPYs(kinkSPY, kink, type(uint32).max, reserveFee); + (r.maxAPY, r.maxSupplyAPY) = computeAPYs(maxSPY, type(uint32).max, type(uint32).max, reserveFee); + } + + + + + // AccountLiquidity queries + + struct ResponseAccountLiquidity { + IRiskManager.AssetLiquidity[] markets; + } + + function doQueryAccountLiquidity(address eulerContract, address[] memory addrs) external view returns (ResponseAccountLiquidity[] memory r) { + Euler eulerProxy = Euler(eulerContract); + IExec execProxy = IExec(eulerProxy.moduleIdToProxy(MODULEID__EXEC)); + + r = new ResponseAccountLiquidity[](addrs.length); + + for (uint i = 0; i < addrs.length; ++i) { + r[i].markets = execProxy.liquidityPerAsset(addrs[i]); + } + } + + + + // For tokens like MKR which return bytes32 on name() or symbol() + + function getStringOrBytes32(address contractAddress, bytes4 selector) private view returns (string memory) { + (bool success, bytes memory result) = contractAddress.staticcall(abi.encodeWithSelector(selector)); + if (!success) return ""; + + return result.length == 32 ? string(abi.encodePacked(result)) : abi.decode(result, (string)); + } +} diff --git a/docs/tasks.md b/docs/tasks.md index f431c97f..72237102 100644 --- a/docs/tasks.md +++ b/docs/tasks.md @@ -91,7 +91,7 @@ npx hardhat --network ropsten uniswap:read-twap USDC ref 3000 1800 ## Deploy a non-module contract -npx hardhat --network ropsten module:deploy EulerGeneralView +npx hardhat --network ropsten module:deploy EulerLensV1 -> update address in addresses/euler-addresses-ropsten.json diff --git a/tasks/debug.js b/tasks/debug.js index 20d61632..0133274d 100644 --- a/tasks/debug.js +++ b/tasks/debug.js @@ -68,11 +68,11 @@ task("debug:swap-contracts", "Replace contract code for all euler contracts on m args: stringifyArgs([ethers.constants.AddressZero, ethers.constants.AddressZero]), }) - console.log('swapping', 'EulerGeneralView'); + console.log('swapping', 'EulerLensV1'); await hre.run('debug:set-code', { compile: false, - name: 'EulerGeneralView', - address: ctx.addressManifest.eulerGeneralView, + name: 'EulerLensV1', + address: ctx.addressManifest.eulerLensV1, args: stringifyArgs([gitCommit]), }) @@ -84,10 +84,34 @@ task("debug:swap-contracts", "Replace contract code for all euler contracts on m args: stringifyArgs([ctx.addressManifest.euler, ctx.addressManifest.exec, ctx.addressManifest.markets]), }) + console.log('swapping', 'SwapHandler1Inch'); + await hre.run('debug:set-code', { + compile: false, + name: 'SwapHandler1Inch', + address: ctx.addressManifest.swapHandlers.SwapHandler1Inch, + args: stringifyArgs([ctx.tokenSetup.existingContracts.oneInch, ctx.tokenSetup.existingContracts.swapRouterV2, ctx.tokenSetup.existingContracts.swapRouterV3]), + }) + + console.log('swapping', 'SwapHandlerUniAutoRouter'); + await hre.run('debug:set-code', { + compile: false, + name: 'SwapHandlerUniAutoRouter', + address: ctx.addressManifest.swapHandlers.SwapHandlerUniAutoRouter, + args: stringifyArgs([ctx.tokenSetup.existingContracts.swapRouter02, ctx.tokenSetup.existingContracts.swapRouterV2, ctx.tokenSetup.existingContracts.swapRouterV3]), + }) + + console.log('swapping', 'SwapHandlerUniswapV3'); + await hre.run('debug:set-code', { + compile: false, + name: 'SwapHandlerUniswapV3', + address: ctx.addressManifest.swapHandlers.SwapHandlerUniswapV3, + args: stringifyArgs([ctx.tokenSetup.existingContracts.swapRouterV3]), + }) + for (const [module, address] of Object.entries(ctx.addressManifest.modules)) { const args = [gitCommit]; if (module === 'riskManager') args.push(ctx.tokenSetup.riskManagerSettings); - if (module === 'swap') args.push(ctx.tokenSetup.existingContracts.swapRouter, ctx.tokenSetup.existingContracts.oneInch); + if (module === 'swap') continue; console.log('swapping', capitalize(module)); await hre.run('debug:set-code', { @@ -177,7 +201,6 @@ task("debug:fork", "Reset localhost network to mainnet fork at a given block or .addOptionalParam("block", "Fork mainnet at the given block") .addOptionalParam("time", "Fork mainnet at the latest block before given time (ISO 8601 / RFC 2822, e.g. 2021-12-28T14:06:40Z)") .setAction(async ({ block, time }) => { - if (network.name !== 'localhost') throw "forkat only on localhost network"; if (block && time) throw 'Block and time params can\'t be used simultaneously'; if (!(block || time)) throw 'Block or time param must be provided'; if (!process.env.RPC_URL_MAINNET) throw 'env variable RPC_URL_MAINNET not found'; @@ -212,7 +235,6 @@ task("debug:set-code", "Set contract code at a given address") .addFlag("compile", "Compile contracts before swapping the code") .addOptionalParam("artifacts", "Path to artifacts file which contains the init bytecode") .setAction(async ({ name, address, args = [], compile, artifacts}) => { - if (network.name !== 'localhost') throw 'Only on localhost network!'; if (name && artifacts) throw 'Name and artifacts params can\'t be used simultaneously'; if (!(name || artifacts)) throw 'Name or artifacts param must be provided'; diff --git a/tasks/liquidate.js b/tasks/liquidate.js index 2af96e79..898ea30d 100644 --- a/tasks/liquidate.js +++ b/tasks/liquidate.js @@ -4,7 +4,7 @@ task("liquidate:check") const et = require("../test/lib/eTestLib"); const ctx = await et.getTaskCtx(); - let detLiq = await ctx.contracts.exec.callStatic.detailedLiquidity(args.violator); + let detLiq = await ctx.contracts.exec.callStatic.liquidityPerAsset(args.violator); let markets = []; diff --git a/tasks/view.js b/tasks/view.js index 13d84f3b..66fee8c2 100644 --- a/tasks/view.js +++ b/tasks/view.js @@ -6,7 +6,7 @@ task("view") let market = await et.taskUtils.lookupToken(ctx, args.market); - let res = await ctx.contracts.eulerGeneralView.callStatic.doQuery({ eulerContract: ctx.contracts.euler.address, account: et.AddressZero, markets: [market.address], }); + let res = await ctx.contracts.eulerLensV1.callStatic.doQuery({ eulerContract: ctx.contracts.euler.address, account: et.AddressZero, markets: [market.address], }); console.log(et.dumpObj(res)); }); @@ -18,7 +18,7 @@ task("view:account") const et = require("../test/lib/eTestLib"); const ctx = await et.getTaskCtx(); - let res = await ctx.contracts.eulerGeneralView.callStatic.doQuery({ eulerContract: ctx.contracts.euler.address, account: args.addr, markets: [], }); + let res = await ctx.contracts.eulerLensV1.callStatic.doQuery({ eulerContract: ctx.contracts.euler.address, account: args.addr, markets: [], }); console.log(et.dumpObj(res)); }); @@ -30,7 +30,7 @@ task("view:detailedLiquidity") const et = require("../test/lib/eTestLib"); const ctx = await et.getTaskCtx(); - let res = await ctx.contracts.exec.callStatic.detailedLiquidity(args.addr); + let res = await ctx.contracts.exec.callStatic.liquidityPerAsset(args.addr); console.log(et.dumpObj(res)); }); @@ -45,7 +45,7 @@ task("view:queryIRM") let market = await et.taskUtils.lookupToken(ctx, args.market); - let res = await ctx.contracts.eulerGeneralView.doQueryIRM({ eulerContract: ctx.contracts.euler.address, underlying: market.address, }); + let res = await ctx.contracts.eulerLensV1.doQueryIRM({ eulerContract: ctx.contracts.euler.address, underlying: market.address, }); console.log(et.dumpObj(res)); }); diff --git a/test/balancesWithInterest.js b/test/balancesWithInterest.js index d2e4fd5c..8ba3123e 100644 --- a/test/balancesWithInterest.js +++ b/test/balancesWithInterest.js @@ -113,7 +113,7 @@ et.testSet({ { from: ctx.wallet4, send: 'dTokens.dTST.borrow', args: [0, et.eth(1)], }, { action: 'checkpointTime', }, - { call: 'eulerGeneralView.doQuery', args: [{ eulerContract: ctx.contracts.euler.address, account: et.AddressZero, markets: [ctx.contracts.tokens.TST.address], }], onResult: r => { + { call: 'eulerLensV1.doQuery', args: [{ eulerContract: ctx.contracts.euler.address, account: et.AddressZero, markets: [ctx.contracts.tokens.TST.address], }], onResult: r => { let tst = r.markets[0]; et.equals(tst.borrowAPY, et.units('0.105244346078570209478701625', 27)); et.equals(tst.supplyAPY, et.units('0.094239711147365655602112334', 27), et.units(et.DefaultReserve, 27)); @@ -181,7 +181,7 @@ et.testSet({ { from: ctx.wallet4, send: 'dTokens.dTST.borrow', args: [0, et.eth(1)], }, { action: 'checkpointTime', }, - { call: 'eulerGeneralView.doQuery', args: [{ eulerContract: ctx.contracts.euler.address, account: et.AddressZero, markets: [ctx.contracts.tokens.TST.address], }], onResult: r => { + { call: 'eulerLensV1.doQuery', args: [{ eulerContract: ctx.contracts.euler.address, account: et.AddressZero, markets: [ctx.contracts.tokens.TST.address], }], onResult: r => { let tst = r.markets[0]; et.equals(tst.borrowAPY, et.units('0.105244346078570209478701625', 27)); et.equals(tst.supplyAPY, et.units('0.046059133709789858497725776', 27)); @@ -206,7 +206,7 @@ et.testSet({ // Get new APYs: - { call: 'eulerGeneralView.doQuery', args: [{ eulerContract: ctx.contracts.euler.address, account: et.AddressZero, markets: [ctx.contracts.tokens.TST.address], }], onResult: r => { + { call: 'eulerLensV1.doQuery', args: [{ eulerContract: ctx.contracts.euler.address, account: et.AddressZero, markets: [ctx.contracts.tokens.TST.address], }], onResult: r => { let tst = r.markets[0]; et.equals(tst.borrowAPY, et.units('0.105244346078570209478701625', 27)); et.equals(tst.supplyAPY, et.units('0.048416583057772105844407061', 27)); @@ -241,7 +241,7 @@ et.testSet({ { from: ctx.wallet4, send: 'dTokens.dTST.borrow', args: [0, et.eth(1)], }, - { call: 'eulerGeneralView.doQuery', args: [{ eulerContract: ctx.contracts.euler.address, account: et.AddressZero, markets: [ctx.contracts.tokens.TST.address], }], onResult: r => { + { call: 'eulerLensV1.doQuery', args: [{ eulerContract: ctx.contracts.euler.address, account: et.AddressZero, markets: [ctx.contracts.tokens.TST.address], }], onResult: r => { let tst = r.markets[0]; et.equals(tst.borrowAPY, et.units('0.105244346078570209478701625', 27)); et.equals(tst.supplyAPY, et.units('0.094239711147365655602112334', 27), '0.00000001'); @@ -250,7 +250,7 @@ et.testSet({ { from: ctx.wallet2, send: 'tokens.TST.transfer', args: [ctx.contracts.euler.address, et.eth(1)], }, { action: 'checkpointTime', }, - { call: 'eulerGeneralView.doQuery', args: [{ eulerContract: ctx.contracts.euler.address, account: et.AddressZero, markets: [ctx.contracts.tokens.TST.address], }], onResult: r => { + { call: 'eulerLensV1.doQuery', args: [{ eulerContract: ctx.contracts.euler.address, account: et.AddressZero, markets: [ctx.contracts.tokens.TST.address], }], onResult: r => { let tst = r.markets[0]; et.equals(tst.borrowAPY, et.units('0.105244346078570209478701625', 27)); et.equals(tst.supplyAPY, et.units('0.0460591337844726578053667', 27)); diff --git a/test/batch.js b/test/batch.js index c371f645..bd0f8730 100644 --- a/test/batch.js +++ b/test/batch.js @@ -30,15 +30,15 @@ et.testSet({ { send: 'eTokens.eTST.transfer', args: [et.getSubAccount(ctx.wallet.address, 3), et.eth(1)], }, { send: 'eTokens.eTST.transferFrom', args: [et.getSubAccount(ctx.wallet.address, 1), et.getSubAccount(ctx.wallet.address, 2), et.eth(.6)], }, { send: 'markets.enterMarket', args: [1, ctx.contracts.tokens.TST.address], }, - { send: 'exec.detailedLiquidity', args: [et.getSubAccount(ctx.wallet.address, 1)]}, - { send: 'exec.detailedLiquidity', args: [et.getSubAccount(ctx.wallet.address, 2)]}, - { send: 'exec.detailedLiquidity', args: [ctx.wallet.address]}, + { send: 'exec.liquidityPerAsset', args: [et.getSubAccount(ctx.wallet.address, 1)]}, + { send: 'exec.liquidityPerAsset', args: [et.getSubAccount(ctx.wallet.address, 2)]}, + { send: 'exec.liquidityPerAsset', args: [ctx.wallet.address]}, ], deferLiquidityChecks: [ctx.wallet.address], simulate: true, onResult: r => { const liquidities = [4, 5, 6].map(i => { - const decoded = ctx.contracts.exec.interface.decodeFunctionResult('detailedLiquidity', r[i].result); + const decoded = ctx.contracts.exec.interface.decodeFunctionResult('liquidityPerAsset', r[i].result); return decoded.assets; }) @@ -171,18 +171,18 @@ et.testSet({ { action: 'sendBatch', simulate: true, batch: [ { send: 'eTokens.eTST.transfer', args: [et.getSubAccount(ctx.wallet.address, 1), et.eth(1)], }, { send: 'exec.doStaticCall' ,args: [ - ctx.contracts.eulerGeneralView.address, - ctx.contracts.eulerGeneralView.interface.encodeFunctionData('doQuery', [{ + ctx.contracts.eulerLensV1.address, + ctx.contracts.eulerLensV1.interface.encodeFunctionData('doQuery', [{ eulerContract: ctx.contracts.euler.address, account: ctx.wallet.address, markets: [ctx.contracts.tokens.TST.address], }]), ]}, ], onResult: r => { - [ ctx.stash.a ] = ctx.contracts.eulerGeneralView.interface.decodeFunctionResult('doQuery', r[1].result); + [ ctx.stash.a ] = ctx.contracts.eulerLensV1.interface.decodeFunctionResult('doQuery', r[1].result); }}, { send: 'eTokens.eTST.transfer', args: [et.getSubAccount(ctx.wallet.address, 1), et.eth(1)], }, - { call: 'eulerGeneralView.doQuery', args: [{ + { call: 'eulerLensV1.doQuery', args: [{ eulerContract: ctx.contracts.euler.address, account: ctx.wallet.address, markets: [ctx.contracts.tokens.TST.address], @@ -202,9 +202,9 @@ et.testSet({ { action: 'sendBatch', simulate: true, deferLiquidityChecks: [ctx.wallet.address], batch: [ { send: 'dTokens.dTST2.borrow', args: [0, et.eth(10)], }, - { send: 'exec.detailedLiquidity', args: [ctx.wallet.address]}, + { send: 'exec.liquidityPerAsset', args: [ctx.wallet.address]}, ], onResult: r => { - const res = ctx.contracts.exec.interface.decodeFunctionResult('detailedLiquidity', r[1].result) + const res = ctx.contracts.exec.interface.decodeFunctionResult('liquidityPerAsset', r[1].result) const [collateral, liabilities] = res.assets.reduce(([c, l], { status }) => [ status.collateralValue.add(c), status.liabilityValue.add(l), diff --git a/test/borrowIsolation.js b/test/borrowIsolation.js index 16efcc0d..a4ad8755 100644 --- a/test/borrowIsolation.js +++ b/test/borrowIsolation.js @@ -209,37 +209,4 @@ et.testSet({ }) -.test({ - desc: "adding isolated to non-isolated", - actions: ctx => [ - // Setup wallet2 with two non-isolated borrows: - - { send: 'tokens.TST3.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, - { send: 'tokens.TST3.mint', args: [ctx.wallet.address, et.eth(100)], }, - { send: 'eTokens.eTST3.deposit', args: [0, et.eth(10)], }, - - { action: 'setAssetConfig', tok: 'TST', config: { borrowIsolated: false, }, }, - { action: 'setAssetConfig', tok: 'TST3', config: { borrowIsolated: false, }, }, - - { from: ctx.wallet2, send: 'dTokens.dTST.borrow', args: [0, et.eth(.01)], }, - { from: ctx.wallet2, send: 'dTokens.dTST3.borrow', args: [0, et.eth(.01)], }, - - // Show that depositing will cause a self-collateralised loan and therefore an isolation violation: - - { from: ctx.wallet2, send: 'tokens.TST.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, - { from: ctx.wallet2, send: 'tokens.TST.mint', args: [ctx.wallet2.address, et.eth(100)], }, - { from: ctx.wallet2, send: 'tokens.TST3.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, - { from: ctx.wallet2, send: 'tokens.TST3.mint', args: [ctx.wallet2.address, et.eth(100)], }, - - { from: ctx.wallet2, send: 'eTokens.eTST.deposit', args: [0, et.eth(1)], expectError: 'e/borrow-isolation-violation', }, - { from: ctx.wallet2, send: 'eTokens.eTST3.deposit', args: [0, et.eth(1)], expectError: 'e/borrow-isolation-violation', }, - - // Same with transferring eTokens to this wallet: - - { send: 'eTokens.eTST.transfer', args: [ctx.wallet2.address, et.eth(1)], expectError: 'e/borrow-isolation-violation', }, - { send: 'eTokens.eTST3.transfer', args: [ctx.wallet2.address, et.eth(1)], expectError: 'e/borrow-isolation-violation', }, - ], -}) - - .run(); diff --git a/test/lib/eTestLib.js b/test/lib/eTestLib.js index 4c90ae81..d21b2167 100644 --- a/test/lib/eTestLib.js +++ b/test/lib/eTestLib.js @@ -17,8 +17,6 @@ const { verifyBatch } = require("./deployLib"); Error.stackTraceLimit = 10000; let conf; - - const moduleIds = { // Public single-proxy modules INSTALLER: 1, @@ -89,7 +87,7 @@ const contractNames = [ 'TestERC20', 'MockUniswapV3Factory', - 'EulerGeneralView', + 'EulerLensV1', 'InvariantChecker', 'FlashLoanNativeTest', 'FlashLoanAdaptorTest', @@ -979,9 +977,9 @@ async function deployContracts(provider, wallets, tokenSetupName, verify = null) address: ctx.contracts.eulerSimpleLens.address, args: [gitCommit, ctx.contracts.euler.address], contractPath: "contracts/views/EulerSimpleLens.sol:EulerSimpleLens" }; - ctx.contracts.eulerGeneralView = await (await ctx.factories.EulerGeneralView.deploy(gitCommit)).deployed(); - verification.contracts.eulerGeneralView = { - address: ctx.contracts.eulerGeneralView.address, args: [gitCommit], contractPath: "contracts/views/EulerGeneralView.sol:EulerGeneralView" + ctx.contracts.eulerLensV1 = await (await ctx.factories.EulerLensV1.deploy(gitCommit)).deployed(); + verification.contracts.eulerLensV1 = { + address: ctx.contracts.eulerLensV1.address, args: [gitCommit], contractPath: "contracts/views/EulerLensV1.sol:EulerLensV1" }; // Get reference to installer proxy diff --git a/test/liquidation.js b/test/liquidation.js index 73851082..3a2adf30 100644 --- a/test/liquidation.js +++ b/test/liquidation.js @@ -15,6 +15,7 @@ et.testSet({ actions.push({ action: 'setIRM', underlying: 'WETH', irm: 'IRM_ZERO', }); actions.push({ action: 'setIRM', underlying: 'TST', irm: 'IRM_ZERO', }); actions.push({ action: 'setIRM', underlying: 'TST2', irm: 'IRM_ZERO', }); + actions.push({ action: 'setIRM', underlying: 'TST3', irm: 'IRM_ZERO', }); actions.push({ action: 'setAssetConfig', tok: 'WETH', config: { borrowFactor: .4}, }); actions.push({ action: 'setAssetConfig', tok: 'TST', config: { borrowFactor: .4}, }); actions.push({ action: 'setAssetConfig', tok: 'TST2', config: { borrowFactor: .4}, }); @@ -300,7 +301,6 @@ et.testSet({ - .test({ desc: "multiple borrows", actions: ctx => [ @@ -418,7 +418,7 @@ et.testSet({ .test({ - desc: "Minimal collateral factor", + desc: "minimal collateral factor", actions: ctx => [ { from: ctx.wallet2, send: 'dTokens.dTST.borrow', args: [0, et.eth(5)], }, @@ -629,7 +629,6 @@ et.testSet({ }, }, - // for the rest of the tracking period liquidator's assets = violator's liability { send: 'eTokens.eTST2.deposit', args: [0, et.eth(50)], }, @@ -913,7 +912,7 @@ et.testSet({ { action: 'sendBatch', batch: [ { send: 'liquidation.liquidate', args: [ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST2.address, () => ctx.stash.repay, 0], }, - { from: ctx.wallet, send: 'dTokens.dTST.repay', args: [0, ctx.stash.repay], }, + { from: ctx.wallet, send: 'dTokens.dTST.repay', args: [0, () => ctx.stash.repay], }, ], deferLiquidityChecks: [ctx.wallet.address], }, @@ -1140,4 +1139,34 @@ et.testSet({ }) + + + +.test({ + desc: "zero borrow factor allows liquidation of the full debt", + actions: ctx => [ + { from: ctx.wallet2, send: 'dTokens.dTST.borrow', args: [0, et.eth(5)], }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + ctx.stash.collateralValue = r.collateralValue; + et.equals(r.collateralValue / r.liabilityValue, 1.09, 0.01); + }, }, + + { action: 'setAssetConfig', tok: 'TST', config: { borrowFactor: 0}, }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.collateralValue, ctx.stash.collateralValue); + et.equals(r.liabilityValue, et.BN(2).pow(144).sub(1)); // max sane debt + }, }, + + { callStatic: 'liquidation.checkLiquidation', args: [ctx.wallet.address, ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST2.address], + onResult: r => { + et.equals(r.healthScore, 0, 0.001); + et.equals(r.repay, 5.1); // full debt + reserve fees + et.equals(r.yield, 34.37, 0.01); // 20% discount 2.2 / (.4 * (1 - 0.2)) * 5 + }, + }, + ], +}) + .run(); diff --git a/test/liquidationWithOverrides.js b/test/liquidationWithOverrides.js new file mode 100644 index 00000000..1a553d7e --- /dev/null +++ b/test/liquidationWithOverrides.js @@ -0,0 +1,1484 @@ +const et = require('./lib/eTestLib'); + +const testDetailedLiability = (ctx, expectedHs) => + ({ call: 'exec.liquidityPerAsset', args: [ctx.wallet2.address], onResult: r => { + const [collateral, liabilities] = r.reduce(([c, l], { status }) => [ + status.collateralValue.add(c), + status.liabilityValue.add(l), + ], [0, 0]) + et.equals(collateral / liabilities, expectedHs, 0.001); + }}); + + + +et.testSet({ + desc: "liquidation with overrides", + + preActions: ctx => { + let actions = []; + + actions.push({ action: 'setIRM', underlying: 'WETH', irm: 'IRM_ZERO', }); + actions.push({ action: 'setIRM', underlying: 'TST', irm: 'IRM_ZERO', }); + actions.push({ action: 'setIRM', underlying: 'TST2', irm: 'IRM_ZERO', }); + actions.push({ action: 'setIRM', underlying: 'TST3', irm: 'IRM_ZERO', }); + actions.push({ action: 'setAssetConfig', tok: 'WETH', config: { borrowFactor: .4}, }); + actions.push({ action: 'setAssetConfig', tok: 'TST', config: { borrowFactor: .4}, }); + actions.push({ action: 'setAssetConfig', tok: 'TST2', config: { borrowFactor: .4}, }); + + // wallet is lender and liquidator + + actions.push({ send: 'tokens.TST.mint', args: [ctx.wallet.address, et.eth(200)], }); + actions.push({ send: 'tokens.TST.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }); + actions.push({ send: 'eTokens.eTST.deposit', args: [0, et.eth(100)], }); + + actions.push({ send: 'tokens.WETH.mint', args: [ctx.wallet.address, et.eth(200)], }); + actions.push({ send: 'tokens.WETH.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }); + actions.push({ send: 'eTokens.eWETH.deposit', args: [0, et.eth(100)], }); + + // wallet2 is borrower/violator + + actions.push({ send: 'tokens.TST2.mint', args: [ctx.wallet2.address, et.eth(100)], }); + actions.push({ from: ctx.wallet2, send: 'tokens.TST2.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }); + actions.push({ from: ctx.wallet2, send: 'eTokens.eTST2.deposit', args: [0, et.eth(100)], }); + actions.push({ from: ctx.wallet2, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST2.address], },); + + // wallet3 is innocent bystander + + actions.push({ send: 'tokens.TST.mint', args: [ctx.wallet3.address, et.eth(100)], }); + actions.push({ from: ctx.wallet3, send: 'tokens.TST.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }); + actions.push({ from: ctx.wallet3, send: 'eTokens.eTST.deposit', args: [0, et.eth(30)], }); + actions.push({ send: 'tokens.TST2.mint', args: [ctx.wallet3.address, et.eth(100)], }); + actions.push({ from: ctx.wallet3, send: 'tokens.TST2.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }); + actions.push({ from: ctx.wallet3, send: 'eTokens.eTST2.deposit', args: [0, et.eth(18)], }); + + // initial prices + + actions.push({ action: 'updateUniswapPrice', pair: 'TST/WETH', price: '2.2', }); + actions.push({ action: 'updateUniswapPrice', pair: 'TST2/WETH', price: '.4', }); + actions.push({ action: 'updateUniswapPrice', pair: 'TST3/WETH', price: '1.7', }); + actions.push({ action: 'updateUniswapPrice', pair: 'TST6/WETH', price: '1.3', }); + + return actions; + }, +}) + + + +.test({ + desc: "I extra regular collateral, result liability fully covered by override", + actions: ctx => [ + { action: 'setAssetConfig', tok: 'TST3', config: { collateralFactor: .5}, }, + { send: 'tokens.TST3.mint', args: [ctx.wallet2.address, et.eth(100)], }, + { from: ctx.wallet2, send: 'tokens.TST3.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { from: ctx.wallet2, send: 'eTokens.eTST3.deposit', args: [0, et.eth(1)], }, + { from: ctx.wallet2, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST3.address], }, + + { from: ctx.wallet2, send: 'dTokens.dTST.borrow', args: [0, et.eth(5)], }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.collateralValue / r.liabilityValue, 1.12, 0.01); + et.assert(r.overrideCollateralValue.eq(0)); + }, }, + testDetailedLiability(ctx, 1.121), + + { action: 'snapshot'}, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST2.address, + { + enabled: true, + collateralFactor: Math.floor(0.9 * 4e9), + }, + ], }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.collateralValue / r.liabilityValue, 3.35, 0.01); + et.assert(r.overrideCollateralValue.gt(0)); + }, }, + testDetailedLiability(ctx, 3.35), + + { action: 'updateUniswapPrice', pair: 'TST/WETH', price: '7.4', }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.collateralValue / r.liabilityValue, 0.957, 0.001); + }, }, + testDetailedLiability(ctx, 0.957), + + { callStatic: 'liquidation.checkLiquidation', args: [ctx.wallet.address, ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST2.address], + onResult: r => { + et.equals(r.healthScore, 0.957, 0.001); + ctx.stash.repay = r.repay; + ctx.stash.yield = r.yield; + }, + }, + + // Successful liquidation + + { call: 'eTokens.eTST.reserveBalanceUnderlying', args: [], equals: [0, '0.000000000001'] }, + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: et.eth('5'), }, + + { send: 'liquidation.liquidate', args: [ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST2.address, () => ctx.stash.repay, 0], }, + + // liquidator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet.address], equals: () => ctx.stash.repay, }, + { call: 'eTokens.eTST2.balanceOfUnderlying', args: [ctx.wallet.address], equals: () => [ctx.stash.yield, '0.000000000001'], }, + + // reserves: + { call: 'eTokens.eTST.reserveBalanceUnderlying', onResult: (r) => ctx.stash.reserves = r, }, + + // violator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: () => [et.units(5).sub(ctx.stash.repay).add(ctx.stash.reserves), '0.000000000001'], }, + { call: 'eTokens.eTST2.balanceOfUnderlying', args: [ctx.wallet2.address], equals: () => [et.units(100).sub(ctx.stash.yield), '0.000000000001'], }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: async (r) => { + let targetHealth = (await ctx.contracts.liquidation.TARGET_HEALTH()) / 1e18; + et.equals(r.collateralValue / r.liabilityValue, targetHealth, 0.00000001); + }}, + testDetailedLiability(ctx, 1.25), + ], +}) + + + + + +.test({ + desc: "II only override collateral", + actions: ctx => [ + { from: ctx.wallet2, send: 'dTokens.dTST.borrow', args: [0, et.eth(4.9)], }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.collateralValue / r.liabilityValue, 1.113, 0.001); + et.assert(r.overrideCollateralValue.eq(0)); + }, }, + testDetailedLiability(ctx, 1.113), + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST2.address, + { + enabled: true, + collateralFactor: Math.floor(0.9 * 4e9), + }, + ], }, + + + { action: 'updateUniswapPrice', pair: 'TST/WETH', price: '7.4', }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.collateralValue / r.liabilityValue, 0.982, 0.001); + }, }, + testDetailedLiability(ctx, 0.982), + + { callStatic: 'liquidation.checkLiquidation', args: [ctx.wallet.address, ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST2.address], + onResult: r => { + et.equals(r.healthScore, 0.982, 0.001); + ctx.stash.repay = r.repay; + ctx.stash.yield = r.yield; + }, + }, + + // Successful liquidation + + { call: 'eTokens.eTST.reserveBalanceUnderlying', args: [], equals: [0, '0.000000000001'] }, + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: et.eth('4.9'), }, + + { send: 'liquidation.liquidate', args: [ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST2.address, () => ctx.stash.repay, 0], }, + + // liquidator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet.address], equals: () => ctx.stash.repay, }, + { call: 'eTokens.eTST2.balanceOfUnderlying', args: [ctx.wallet.address], equals: () => [ctx.stash.yield, '0.000000000001'], }, + + // reserves: + { call: 'eTokens.eTST.reserveBalanceUnderlying', onResult: (r) => ctx.stash.reserves = r, }, + + // violator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: () => [et.units(4.9).sub(ctx.stash.repay).add(ctx.stash.reserves), '0.000000000001'], }, + { call: 'eTokens.eTST2.balanceOfUnderlying', args: [ctx.wallet2.address], equals: () => [et.units(100).sub(ctx.stash.yield), '0.000000000001'], }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: async (r) => { + let targetHealth = (await ctx.contracts.liquidation.TARGET_HEALTH()) / 1e18; + et.equals(r.collateralValue / r.liabilityValue, targetHealth, 0.00000001); + }}, + testDetailedLiability(ctx, 1.25), + ], +}) + + + + + +.test({ + desc: "III extra regular collateral, liquidation doesn't improve health score", + actions: ctx => [ + { action: 'setAssetConfig', tok: 'TST3', config: { collateralFactor: .5}, }, + { send: 'tokens.TST3.mint', args: [ctx.wallet2.address, et.eth(100)], }, + { from: ctx.wallet2, send: 'tokens.TST3.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { from: ctx.wallet2, send: 'eTokens.eTST3.deposit', args: [0, et.eth(40)], }, + { from: ctx.wallet2, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST3.address], }, + + { from: ctx.wallet2, send: 'dTokens.dTST.borrow', args: [0, et.eth(7)], }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST2.address, + { + enabled: true, + collateralFactor: Math.floor(0.9 * 4e9), + }, + ], }, + + { action: 'updateUniswapPrice', pair: 'TST/WETH', price: '7.4', }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.collateralValue / r.liabilityValue, 0.927, 0.001); + }, }, + testDetailedLiability(ctx, 0.927), + + { callStatic: 'liquidation.checkLiquidation', args: [ctx.wallet.address, ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST2.address], + onResult: r => { + et.equals(r.healthScore, 0.927, 0.001); + et.equals(r.yield, 100, 0.00001) + ctx.stash.repay = r.repay; + ctx.stash.yield = r.yield; + }, + }, + + // Successful liquidation + + { call: 'eTokens.eTST.reserveBalanceUnderlying', args: [], equals: [0, '0.000000000001'] }, + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: et.eth('7'), }, + + { send: 'liquidation.liquidate', args: [ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST2.address, () => ctx.stash.repay, 0], }, + + // liquidator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet.address], equals: () => ctx.stash.repay, }, + { call: 'eTokens.eTST2.balanceOfUnderlying', args: [ctx.wallet.address], equals: () => [ctx.stash.yield, '0.000000000001'], }, + + // reserves: + { call: 'eTokens.eTST.reserveBalanceUnderlying', onResult: (r) => ctx.stash.reserves = r, }, + + // violator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: () => [et.units(7).sub(ctx.stash.repay).add(ctx.stash.reserves), '0.000000000001'], }, + { call: 'eTokens.eTST2.balanceOfUnderlying', args: [ctx.wallet2.address], equals: () => [et.units(100).sub(ctx.stash.yield), '0.000000000001'], }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: async (r) => { + et.equals(r.collateralValue / r.liabilityValue, 0.8767, 0.0001); + }}, + testDetailedLiability(ctx, 0.876), + ], +}) + + + + + +.test({ + desc: "IV extra regular collateral, result liability not fully covered by override", + actions: ctx => [ + { action: 'setAssetConfig', tok: 'TST3', config: { collateralFactor: .5}, }, + { send: 'tokens.TST3.mint', args: [ctx.wallet2.address, et.eth(100)], }, + { from: ctx.wallet2, send: 'tokens.TST3.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { from: ctx.wallet2, send: 'eTokens.eTST3.deposit', args: [0, et.eth(20)], }, + { from: ctx.wallet2, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST3.address], }, + + { from: ctx.wallet2, send: 'dTokens.dTST.borrow', args: [0, et.eth(5.8)], }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST2.address, + { + enabled: true, + collateralFactor: Math.floor(0.9 * 4e9), + }, + ], }, + + { action: 'updateUniswapPrice', pair: 'TST/WETH', price: '7.4', }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.collateralValue / r.liabilityValue, 0.994, 0.001); + }, }, + testDetailedLiability(ctx, 0.994), + + { callStatic: 'liquidation.checkLiquidation', args: [ctx.wallet.address, ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST2.address], + onResult: r => { + et.equals(r.healthScore, 0.994, 0.001); + ctx.stash.repay = r.repay; + ctx.stash.yield = r.yield; + }, + }, + + // Successful liquidation + + { call: 'eTokens.eTST.reserveBalanceUnderlying', args: [], equals: [0, '0.000000000001'] }, + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: et.eth('5.8'), }, + + { send: 'liquidation.liquidate', args: [ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST2.address, () => ctx.stash.repay, 0], }, + + // liquidator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet.address], equals: () => ctx.stash.repay, }, + { call: 'eTokens.eTST2.balanceOfUnderlying', args: [ctx.wallet.address], equals: () => [ctx.stash.yield, '0.000000000001'], }, + + // reserves: + { call: 'eTokens.eTST.reserveBalanceUnderlying', onResult: (r) => ctx.stash.reserves = r, }, + + // violator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: () => [et.units(5.8).sub(ctx.stash.repay).add(ctx.stash.reserves), '0.000000000001'], }, + { call: 'eTokens.eTST2.balanceOfUnderlying', args: [ctx.wallet2.address], equals: () => [et.units(100).sub(ctx.stash.yield), '0.000000000001'], }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: async (r) => { + let targetHealth = (await ctx.contracts.liquidation.TARGET_HEALTH()) / 1e18; + et.equals(r.collateralValue / r.liabilityValue, targetHealth, 0.00000001); + }}, + testDetailedLiability(ctx, 1.25), + ], +}) + + + + + +.test({ + desc: "V extra regular collateral and override collateral, result liability not fully covered by override", + actions: ctx => [ + { action: 'setAssetConfig', tok: 'TST3', config: { collateralFactor: .5}, }, + { send: 'tokens.TST3.mint', args: [ctx.wallet2.address, et.eth(100)], }, + { from: ctx.wallet2, send: 'tokens.TST3.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { from: ctx.wallet2, send: 'eTokens.eTST3.deposit', args: [0, et.eth(8)], }, + { from: ctx.wallet2, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST3.address], }, + + { send: 'tokens.TST6.mint', args: [ctx.wallet2.address, et.eth(100)], }, + { from: ctx.wallet2, send: 'tokens.TST6.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { from: ctx.wallet2, send: 'eTokens.eTST6.deposit', args: [0, et.eth(5)], }, + { from: ctx.wallet2, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST6.address], }, + + { from: ctx.wallet2, send: 'dTokens.dTST.borrow', args: [0, et.eth(5.8)], }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST2.address, + { + enabled: true, + collateralFactor: Math.floor(0.9 * 4e9), + }, + ], }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST6.address, + { + enabled: true, + collateralFactor: Math.floor(0.6 * 4e9), + }, + ], }, + + { action: 'updateUniswapPrice', pair: 'TST/WETH', price: '7.4', }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.collateralValue / r.liabilityValue, 0.984, 0.001); + }, }, + testDetailedLiability(ctx, 0.984), + + { callStatic: 'liquidation.checkLiquidation', args: [ctx.wallet.address, ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST2.address], + onResult: r => { + et.equals(r.healthScore, 0.984, 0.001); + ctx.stash.repay = r.repay; + ctx.stash.yield = r.yield; + }, + }, + + // Successful liquidation + + { call: 'eTokens.eTST.reserveBalanceUnderlying', args: [], equals: [0, '0.000000000001'] }, + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: et.eth('5.8'), }, + + { send: 'liquidation.liquidate', args: [ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST2.address, () => ctx.stash.repay, 0], }, + + // liquidator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet.address], equals: () => ctx.stash.repay, }, + { call: 'eTokens.eTST2.balanceOfUnderlying', args: [ctx.wallet.address], equals: () => [ctx.stash.yield, '0.000000000001'], }, + + // reserves: + { call: 'eTokens.eTST.reserveBalanceUnderlying', onResult: (r) => ctx.stash.reserves = r, }, + + // violator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: () => [et.units(5.8).sub(ctx.stash.repay).add(ctx.stash.reserves), '0.000000000001'], }, + { call: 'eTokens.eTST2.balanceOfUnderlying', args: [ctx.wallet2.address], equals: () => [et.units(100).sub(ctx.stash.yield), '0.000000000001'], }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: async (r) => { + let targetHealth = (await ctx.contracts.liquidation.TARGET_HEALTH()) / 1e18; + et.equals(r.collateralValue / r.liabilityValue, targetHealth, 0.00000001); + }}, + testDetailedLiability(ctx, 1.25), + ], +}) + + + + + +.test({ + desc: "VI extra regular collateral and override collateral, result liability fully covered by override", + actions: ctx => [ + { action: 'setAssetConfig', tok: 'TST3', config: { collateralFactor: .5}, }, + { send: 'tokens.TST3.mint', args: [ctx.wallet2.address, et.eth(100)], }, + { from: ctx.wallet2, send: 'tokens.TST3.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { from: ctx.wallet2, send: 'eTokens.eTST3.deposit', args: [0, et.eth(1)], }, + { from: ctx.wallet2, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST3.address], }, + + { send: 'tokens.TST6.mint', args: [ctx.wallet2.address, et.eth(100)], }, + { from: ctx.wallet2, send: 'tokens.TST6.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { from: ctx.wallet2, send: 'eTokens.eTST6.deposit', args: [0, et.eth(8)], }, + { from: ctx.wallet2, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST6.address], }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST6.address, + { + enabled: true, + collateralFactor: Math.floor(0.6 * 4e9), + }, + ], }, + + { from: ctx.wallet2, send: 'dTokens.dTST.borrow', args: [0, et.eth(5.8)], }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST2.address, + { + enabled: true, + collateralFactor: Math.floor(0.9 * 4e9), + }, + ], }, + + + { action: 'updateUniswapPrice', pair: 'TST/WETH', price: '7.4', }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.collateralValue / r.liabilityValue, 0.98, 0.001); + }, }, + testDetailedLiability(ctx, 0.98), + + { callStatic: 'liquidation.checkLiquidation', args: [ctx.wallet.address, ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST2.address], + onResult: r => { + et.equals(r.healthScore, 0.98, 0.001); + ctx.stash.repay = r.repay; + ctx.stash.yield = r.yield; + }, + }, + + // Successful liquidation + + { call: 'eTokens.eTST.reserveBalanceUnderlying', args: [], equals: [0, '0.000000000001'] }, + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: et.eth('5.8'), }, + + { send: 'liquidation.liquidate', args: [ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST2.address, () => ctx.stash.repay, 0], }, + + // liquidator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet.address], equals: () => ctx.stash.repay, }, + { call: 'eTokens.eTST2.balanceOfUnderlying', args: [ctx.wallet.address], equals: () => [ctx.stash.yield, '0.000000000001'], }, + + // reserves: + { call: 'eTokens.eTST.reserveBalanceUnderlying', onResult: (r) => ctx.stash.reserves = r, }, + + // violator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: () => [et.units(5.8).sub(ctx.stash.repay).add(ctx.stash.reserves), '0.000000000001'], }, + { call: 'eTokens.eTST2.balanceOfUnderlying', args: [ctx.wallet2.address], equals: () => [et.units(100).sub(ctx.stash.yield), '0.000000000001'], }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: async (r) => { + let targetHealth = (await ctx.contracts.liquidation.TARGET_HEALTH()) / 1e18; + et.equals(r.collateralValue / r.liabilityValue, targetHealth, 0.00000001); + }}, + testDetailedLiability(ctx, 1.25), + ], +}) + + + + + +.test({ + desc: "VII extra override collateral", + actions: ctx => [ + { send: 'tokens.TST6.mint', args: [ctx.wallet2.address, et.eth(100)], }, + { from: ctx.wallet2, send: 'tokens.TST6.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { from: ctx.wallet2, send: 'eTokens.eTST6.deposit', args: [0, et.eth(8.5)], }, + { from: ctx.wallet2, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST6.address], }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST6.address, + { + enabled: true, + collateralFactor: Math.floor(0.6 * 4e9), + }, + ], }, + + { from: ctx.wallet2, send: 'dTokens.dTST.borrow', args: [0, et.eth(5.8)], }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST2.address, + { + enabled: true, + collateralFactor: Math.floor(0.9 * 4e9), + }, + ], }, + + + { action: 'updateUniswapPrice', pair: 'TST/WETH', price: '7.4', }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.collateralValue / r.liabilityValue, 0.983, 0.001); + }, }, + testDetailedLiability(ctx, 0.983), + + { callStatic: 'liquidation.checkLiquidation', args: [ctx.wallet.address, ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST2.address], + onResult: r => { + et.equals(r.healthScore, 0.983, 0.001); + ctx.stash.repay = r.repay; + ctx.stash.yield = r.yield; + }, + }, + + // Successful liquidation + + { call: 'eTokens.eTST.reserveBalanceUnderlying', args: [], equals: [0, '0.000000000001'] }, + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: et.eth('5.8'), }, + + { send: 'liquidation.liquidate', args: [ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST2.address, () => ctx.stash.repay, 0], }, + + // liquidator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet.address], equals: () => ctx.stash.repay, }, + { call: 'eTokens.eTST2.balanceOfUnderlying', args: [ctx.wallet.address], equals: () => [ctx.stash.yield, '0.000000000001'], }, + + // reserves: + { call: 'eTokens.eTST.reserveBalanceUnderlying', onResult: (r) => ctx.stash.reserves = r, }, + + // violator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: () => [et.units(5.8).sub(ctx.stash.repay).add(ctx.stash.reserves), '0.000000000001'], }, + { call: 'eTokens.eTST2.balanceOfUnderlying', args: [ctx.wallet2.address], equals: () => [et.units(100).sub(ctx.stash.yield), '0.000000000001'], }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: async (r) => { + let targetHealth = (await ctx.contracts.liquidation.TARGET_HEALTH()) / 1e18; + et.equals(r.collateralValue / r.liabilityValue, targetHealth, 0.00000001); + }}, + testDetailedLiability(ctx, 1.25), + ], +}) + + + + + +.test({ + desc: "VIII self-collateral, extra regular and override collateral, result fully covered by override collateral", + actions: ctx => [ + { action: 'setAssetConfig', tok: 'TST3', config: { collateralFactor: .5}, }, + { send: 'tokens.TST3.mint', args: [ctx.wallet2.address, et.eth(100)], }, + { from: ctx.wallet2, send: 'tokens.TST3.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { from: ctx.wallet2, send: 'eTokens.eTST3.deposit', args: [0, et.eth(2)], }, + { from: ctx.wallet2, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST3.address], }, + + { send: 'tokens.TST6.mint', args: [ctx.wallet2.address, et.eth(100)], }, + { from: ctx.wallet2, send: 'tokens.TST6.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { from: ctx.wallet2, send: 'eTokens.eTST6.deposit', args: [0, et.eth(10)], }, + { from: ctx.wallet2, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST6.address], }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST6.address, + { + enabled: true, + collateralFactor: Math.floor(0.6 * 4e9), + }, + ], }, + + { from: ctx.wallet2, send: 'eTokens.eTST.mint', args: [0, et.eth(.7)], }, + { from: ctx.wallet2, send: 'dTokens.dTST.borrow', args: [0, et.eth(6.03)], }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST2.address, + { + enabled: true, + collateralFactor: Math.floor(0.9 * 4e9), + }, + ], }, + + + { action: 'updateUniswapPrice', pair: 'TST/WETH', price: '7.4', }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.collateralValue / r.liabilityValue, 0.98, 0.001); + }, }, + testDetailedLiability(ctx, 0.98), + + { callStatic: 'liquidation.checkLiquidation', args: [ctx.wallet.address, ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST2.address], + onResult: r => { + et.equals(r.healthScore, 0.98, 0.001); + ctx.stash.repay = r.repay; + ctx.stash.yield = r.yield; + }, + }, + + // Successful liquidation + + { call: 'eTokens.eTST.reserveBalanceUnderlying', args: [], equals: [0, '0.000000000001'] }, + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: et.eth('6.73'), }, + + { send: 'liquidation.liquidate', args: [ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST2.address, () => ctx.stash.repay, 0], }, + + // liquidator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet.address], equals: () => ctx.stash.repay, }, + { call: 'eTokens.eTST2.balanceOfUnderlying', args: [ctx.wallet.address], equals: () => [ctx.stash.yield, '0.000000000001'], }, + + // reserves: + { call: 'eTokens.eTST.reserveBalanceUnderlying', onResult: (r) => ctx.stash.reserves = r, }, + + // violator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: () => [et.units(6.73).sub(ctx.stash.repay).add(ctx.stash.reserves), '0.000000000001'], }, + { call: 'eTokens.eTST2.balanceOfUnderlying', args: [ctx.wallet2.address], equals: () => [et.units(100).sub(ctx.stash.yield), '0.000000000001'], }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: async (r) => { + let targetHealth = (await ctx.contracts.liquidation.TARGET_HEALTH()) / 1e18; + et.equals(r.collateralValue / r.liabilityValue, targetHealth, 0.00000001); + }}, + testDetailedLiability(ctx, 1.25), + ], +}) + + + + + +.test({ + desc: "IX Secondary calculation is bounded", + actions: ctx => [ + { action: 'setAssetConfig', tok: 'TST3', config: { collateralFactor: .5}, }, + { send: 'tokens.TST3.mint', args: [ctx.wallet2.address, et.eth(100)], }, + { from: ctx.wallet2, send: 'tokens.TST3.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { from: ctx.wallet2, send: 'eTokens.eTST3.deposit', args: [0, et.eth(20)], }, + { from: ctx.wallet2, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST3.address], }, + + + { send: 'tokens.TST6.mint', args: [ctx.wallet2.address, et.eth(100)], }, + { from: ctx.wallet2, send: 'tokens.TST6.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { from: ctx.wallet2, send: 'eTokens.eTST6.deposit', args: [0, et.eth(10)], }, + { from: ctx.wallet2, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST6.address], }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST6.address, + { + enabled: true, + collateralFactor: Math.floor(0.6 * 4e9), + }, + ], }, + + { from: ctx.wallet2, send: 'eTokens.eTST.mint', args: [0, et.eth(.7)], }, + { from: ctx.wallet2, send: 'dTokens.dTST.borrow', args: [0, et.eth(7)], }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST2.address, + { + enabled: true, + collateralFactor: Math.floor(0.9 * 4e9), + }, + ], }, + + + { action: 'updateUniswapPrice', pair: 'TST/WETH', price: '7.4', }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.collateralValue / r.liabilityValue, 0.947, 0.001); + }, }, + testDetailedLiability(ctx, 0.947), + + { callStatic: 'liquidation.checkLiquidation', args: [ctx.wallet.address, ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST2.address], + onResult: r => { + et.equals(r.healthScore, 0.947, 0.001); + et.equals(r.yield, 100, 0.000001); // bound on liquidation after secondary repay calculation + ctx.stash.repay = r.repay; + ctx.stash.yield = r.yield; + }, + }, + + // Successful liquidation + + { call: 'eTokens.eTST.reserveBalanceUnderlying', args: [], equals: [0, '0.000000000001'] }, + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: et.eth('7.7'), }, + + { send: 'liquidation.liquidate', args: [ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST2.address, () => ctx.stash.repay, 0], }, + + // liquidator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet.address], equals: () => ctx.stash.repay, }, + { call: 'eTokens.eTST2.balanceOfUnderlying', args: [ctx.wallet.address], equals: () => [ctx.stash.yield, '0.000000000001'], }, + + // reserves: + { call: 'eTokens.eTST.reserveBalanceUnderlying', onResult: (r) => ctx.stash.reserves = r, }, + + // violator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: () => [et.units(7.7).sub(ctx.stash.repay).add(ctx.stash.reserves), '0.000000000001'], }, + { call: 'eTokens.eTST2.balanceOfUnderlying', args: [ctx.wallet2.address], equals: () => [et.units(100).sub(ctx.stash.yield), '0.000000000001'], }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: async (r) => { + et.equals(r.collateralValue / r.liabilityValue, 0.97043, 0.00001); + }}, + testDetailedLiability(ctx, 0.97), + ], +}) + + + + + +.test({ + desc: "X Liquidate regular collateral, result not fully supported by override collateral", + actions: ctx => [ + { action: 'setAssetConfig', tok: 'TST3', config: { collateralFactor: .5}, }, + { send: 'tokens.TST3.mint', args: [ctx.wallet2.address, et.eth(200)], }, + { from: ctx.wallet2, send: 'tokens.TST3.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { from: ctx.wallet2, send: 'eTokens.eTST3.deposit', args: [0, et.eth(200)], }, + { from: ctx.wallet2, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST3.address], }, + + + { send: 'tokens.TST6.mint', args: [ctx.wallet2.address, et.eth(100)], }, + { from: ctx.wallet2, send: 'tokens.TST6.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { from: ctx.wallet2, send: 'eTokens.eTST6.deposit', args: [0, et.eth(10)], }, + { from: ctx.wallet2, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST6.address], }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST6.address, + { + enabled: true, + collateralFactor: Math.floor(0.6 * 4e9), + }, + ], }, + + { from: ctx.wallet2, send: 'eTokens.eTST.mint', args: [0, et.eth(.7)], }, + { from: ctx.wallet2, send: 'dTokens.dTST.borrow', args: [0, et.eth(15.3)], }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST2.address, + { + enabled: true, + collateralFactor: Math.floor(0.9 * 4e9), + }, + ], }, + + + { action: 'updateUniswapPrice', pair: 'TST/WETH', price: '7.4', }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.collateralValue / r.liabilityValue, 0.981, 0.001); + }, }, + testDetailedLiability(ctx, 0.981), + + { callStatic: 'liquidation.checkLiquidation', args: [ctx.wallet.address, ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST3.address], + onResult: r => { + et.equals(r.healthScore, 0.981, 0.001); + ctx.stash.repay = r.repay; + ctx.stash.yield = r.yield; + }, + }, + + // Successful liquidation + + { call: 'eTokens.eTST.reserveBalanceUnderlying', args: [], equals: [0, '0.000000000001'] }, + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: et.eth('16'), }, + + { send: 'liquidation.liquidate', args: [ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST3.address, () => ctx.stash.repay, 0], }, + + // liquidator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet.address], equals: () => ctx.stash.repay, }, + { call: 'eTokens.eTST3.balanceOfUnderlying', args: [ctx.wallet.address], equals: () => [ctx.stash.yield, '0.000000000001'], }, + + // reserves: + { call: 'eTokens.eTST.reserveBalanceUnderlying', onResult: (r) => ctx.stash.reserves = r, }, + + // violator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: () => [et.units(16).sub(ctx.stash.repay).add(ctx.stash.reserves), '0.000000000001'], }, + { call: 'eTokens.eTST3.balanceOfUnderlying', args: [ctx.wallet2.address], equals: () => [et.units(200).sub(ctx.stash.yield), '0.000000000001'], }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: async (r) => { + let targetHealth = (await ctx.contracts.liquidation.TARGET_HEALTH()) / 1e18; + et.equals(r.collateralValue / r.liabilityValue, targetHealth, 0.00000001); + }}, + testDetailedLiability(ctx, 1.25), + ], +}) + + + + + +.test({ + desc: "XI Liquidate regular collateral, result fully supported by override collateral", + actions: ctx => [ + { action: 'setAssetConfig', tok: 'TST3', config: { collateralFactor: .5}, }, + { send: 'tokens.TST3.mint', args: [ctx.wallet2.address, et.eth(200)], }, + { from: ctx.wallet2, send: 'tokens.TST3.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { from: ctx.wallet2, send: 'eTokens.eTST3.deposit', args: [0, et.eth(20)], }, + { from: ctx.wallet2, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST3.address], }, + + + { send: 'tokens.TST6.mint', args: [ctx.wallet2.address, et.eth(100)], }, + { from: ctx.wallet2, send: 'tokens.TST6.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { from: ctx.wallet2, send: 'eTokens.eTST6.deposit', args: [0, et.eth(10)], }, + { from: ctx.wallet2, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST6.address], }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST6.address, + { + enabled: true, + collateralFactor: Math.floor(0.6 * 4e9), + }, + ], }, + + { from: ctx.wallet2, send: 'eTokens.eTST.mint', args: [0, et.eth(.7)], }, + { from: ctx.wallet2, send: 'dTokens.dTST.borrow', args: [0, et.eth(7.2)], }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST2.address, + { + enabled: true, + collateralFactor: Math.floor(0.9 * 4e9), + }, + ], }, + + + { action: 'updateUniswapPrice', pair: 'TST/WETH', price: '7.4', }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.collateralValue / r.liabilityValue, 0.899, 0.001); + }, }, + testDetailedLiability(ctx, 0.899), + + { callStatic: 'liquidation.checkLiquidation', args: [ctx.wallet.address, ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST3.address], + onResult: r => { + et.equals(r.healthScore, 0.899, 0.001); + ctx.stash.repay = r.repay; + ctx.stash.yield = r.yield; + }, + }, + + // Successful liquidation + + { call: 'eTokens.eTST.reserveBalanceUnderlying', args: [], equals: [0, '0.000000000001'] }, + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: et.eth('7.9'), }, + + { send: 'liquidation.liquidate', args: [ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST3.address, () => ctx.stash.repay, 0], }, + + // liquidator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet.address], equals: () => ctx.stash.repay, }, + { call: 'eTokens.eTST3.balanceOfUnderlying', args: [ctx.wallet.address], equals: () => [ctx.stash.yield, '0.000000000001'], }, + + // reserves: + { call: 'eTokens.eTST.reserveBalanceUnderlying', onResult: (r) => ctx.stash.reserves = r, }, + + // violator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: () => [et.units(7.9).sub(ctx.stash.repay).add(ctx.stash.reserves), '0.000000000001'], }, + { call: 'eTokens.eTST3.balanceOfUnderlying', args: [ctx.wallet2.address], equals: () => [et.units(20).sub(ctx.stash.yield), '0.000000000001'], }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: async (r) => { + let targetHealth = (await ctx.contracts.liquidation.TARGET_HEALTH()) / 1e18; + et.equals(r.collateralValue / r.liabilityValue, targetHealth, 0.00000001); + }}, + testDetailedLiability(ctx, 1.25), + ], +}) + + + + + +.test({ + desc: "XII Primary calculation yields negative repay", + actions: ctx => [ + { action: 'setAssetConfig', tok: 'TST3', config: { collateralFactor: .9}, }, + { send: 'tokens.TST3.mint', args: [ctx.wallet2.address, et.eth(100)], }, + { from: ctx.wallet2, send: 'tokens.TST3.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { from: ctx.wallet2, send: 'eTokens.eTST3.deposit', args: [0, et.eth(15)], }, + { from: ctx.wallet2, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST3.address], }, + + + { send: 'tokens.TST6.mint', args: [ctx.wallet2.address, et.eth(100)], }, + { from: ctx.wallet2, send: 'tokens.TST6.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { from: ctx.wallet2, send: 'eTokens.eTST6.deposit', args: [0, et.eth(5)], }, + { from: ctx.wallet2, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST6.address], }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST6.address, + { + enabled: true, + collateralFactor: Math.floor(0.6 * 4e9), + }, + ], }, + + { from: ctx.wallet2, send: 'dTokens.dTST.borrow', args: [0, et.eth(3.5)], }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST2.address, + { + enabled: true, + collateralFactor: Math.floor(0.3 * 4e9), + }, + ], }, + + + { action: 'updateUniswapPrice', pair: 'TST/WETH', price: '7.4', }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.collateralValue / r.liabilityValue, 0.949, 0.001); + }, }, + testDetailedLiability(ctx, 0.949), + + { callStatic: 'liquidation.checkLiquidation', args: [ctx.wallet.address, ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST2.address], + onResult: r => { + et.equals(r.healthScore, 0.949, 0.001); + ctx.stash.repay = r.repay; + ctx.stash.yield = r.yield; + }, + }, + + // Successful liquidation + + { call: 'eTokens.eTST.reserveBalanceUnderlying', args: [], equals: [0, '0.000000000001'] }, + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: et.eth('3.5'), }, + + { send: 'liquidation.liquidate', args: [ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST2.address, () => ctx.stash.repay, 0], }, + + // liquidator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet.address], equals: () => ctx.stash.repay, }, + { call: 'eTokens.eTST2.balanceOfUnderlying', args: [ctx.wallet.address], equals: () => [ctx.stash.yield, '0.000000000001'], }, + + // reserves: + { call: 'eTokens.eTST.reserveBalanceUnderlying', onResult: (r) => ctx.stash.reserves = r, }, + + // violator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: () => [et.units(3.5).sub(ctx.stash.repay).add(ctx.stash.reserves), '0.000000000001'], }, + { call: 'eTokens.eTST2.balanceOfUnderlying', args: [ctx.wallet2.address], equals: () => [et.units(100).sub(ctx.stash.yield), '0.000000000001'], }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: async (r) => { + let targetHealth = (await ctx.contracts.liquidation.TARGET_HEALTH()) / 1e18; + et.equals(r.collateralValue / r.liabilityValue, targetHealth, 0.00000001); + }}, + testDetailedLiability(ctx, 1.25), + ], +}) + + + + +.test({ + desc: "XIII Secondary calculation yields negative repay", + actions: ctx => [ + { action: 'setAssetConfig', tok: 'TST3', config: { collateralFactor: .5}, }, + { send: 'tokens.TST3.mint', args: [ctx.wallet2.address, et.eth(200)], }, + { from: ctx.wallet2, send: 'tokens.TST3.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { from: ctx.wallet2, send: 'eTokens.eTST3.deposit', args: [0, et.eth(200)], }, + { from: ctx.wallet2, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST3.address], }, + + + { send: 'tokens.TST6.mint', args: [ctx.wallet2.address, et.eth(100)], }, + { from: ctx.wallet2, send: 'tokens.TST6.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { from: ctx.wallet2, send: 'eTokens.eTST6.deposit', args: [0, et.eth(10)], }, + { from: ctx.wallet2, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST6.address], }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST6.address, + { + enabled: true, + collateralFactor: Math.floor(0.6 * 4e9), + }, + ], }, + + { from: ctx.wallet2, send: 'eTokens.eTST.mint', args: [0, et.eth(.7)], }, + { from: ctx.wallet2, send: 'dTokens.dTST.borrow', args: [0, et.eth(19.3)], }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST2.address, + { + enabled: true, + collateralFactor: Math.floor(0.9 * 4e9), + }, + ], }, + + + { action: 'updateUniswapPrice', pair: 'TST/WETH', price: '7.4', }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.collateralValue / r.liabilityValue, 0.736, 0.001); + }, }, + testDetailedLiability(ctx, 0.736), + + { callStatic: 'liquidation.checkLiquidation', args: [ctx.wallet.address, ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST2.address], + onResult: r => { + et.equals(r.healthScore, 0.736, 0.001); + ctx.stash.repay = r.repay; + ctx.stash.yield = r.yield; + }, + }, + + // Successful liquidation + + { call: 'eTokens.eTST.reserveBalanceUnderlying', args: [], equals: [0, '0.000000000001'] }, + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: et.eth('20'), }, + + { send: 'liquidation.liquidate', args: [ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST2.address, () => ctx.stash.repay, 0], }, + + // liquidator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet.address], equals: () => ctx.stash.repay, }, + { call: 'eTokens.eTST2.balanceOfUnderlying', args: [ctx.wallet.address], equals: () => [ctx.stash.yield, '0.000000000001'], }, + + // reserves: + { call: 'eTokens.eTST.reserveBalanceUnderlying', onResult: (r) => ctx.stash.reserves = r, }, + + // violator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: () => [et.units(20).sub(ctx.stash.repay).add(ctx.stash.reserves), '0.000000000001'], }, + { call: 'eTokens.eTST2.balanceOfUnderlying', args: [ctx.wallet2.address], equals: () => [et.units(100).sub(ctx.stash.yield), '0.000000000001'], }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: async (r) => { + et.equals(r.collateralValue / r.liabilityValue, 0.6744, 0.0001); + }}, + testDetailedLiability(ctx, 0.674), + ], +}) + + + + +.test({ + desc: "XIV Liquidate self-collateral", + actions: ctx => [ + { action: 'setAssetConfig', tok: 'TST3', config: { collateralFactor: .5}, }, + { send: 'tokens.TST3.mint', args: [ctx.wallet2.address, et.eth(200)], }, + { from: ctx.wallet2, send: 'tokens.TST3.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { from: ctx.wallet2, send: 'eTokens.eTST3.deposit', args: [0, et.eth(30)], }, + { from: ctx.wallet2, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST3.address], }, + + + { send: 'tokens.TST6.mint', args: [ctx.wallet2.address, et.eth(100)], }, + { from: ctx.wallet2, send: 'tokens.TST6.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { from: ctx.wallet2, send: 'eTokens.eTST6.deposit', args: [0, et.eth(10)], }, + { from: ctx.wallet2, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST6.address], }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST6.address, + { + enabled: true, + collateralFactor: Math.floor(0.6 * 4e9), + }, + ], }, + + { from: ctx.wallet2, send: 'eTokens.eTST.mint', args: [0, et.eth(50)], }, + { from: ctx.wallet2, send: 'dTokens.dTST.borrow', args: [0, et.eth(5)], }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST2.address, + { + enabled: true, + collateralFactor: Math.floor(0.9 * 4e9), + }, + ], }, + + + { action: 'updateUniswapPrice', pair: 'TST/WETH', price: '7.4', }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.collateralValue / r.liabilityValue, 0.991, 0.001); + }, }, + testDetailedLiability(ctx, 0.991), + + { callStatic: 'liquidation.checkLiquidation', args: [ctx.wallet.address, ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST.address], + onResult: r => { + et.equals(r.healthScore, 0.991, 0.001); + ctx.stash.repay = r.repay; + ctx.stash.yield = r.yield; + }, + }, + + // Successful liquidation + + { call: 'eTokens.eTST.reserveBalanceUnderlying', args: [], equals: [0, '0.000000000001'] }, + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: et.eth('55'), }, + + { send: 'liquidation.liquidate', args: [ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST.address, () => ctx.stash.repay, 0], }, + + // liquidator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet.address], equals: () => ctx.stash.repay, }, + { call: 'eTokens.eTST.balanceOfUnderlying', args: [ctx.wallet.address], equals: () => [ctx.stash.yield.add(et.eth(100)), '0.00001'], }, // 100 pre-existing depsit + + // reserves: + { call: 'eTokens.eTST.reserveBalanceUnderlying', onResult: (r) => ctx.stash.reserves = r, }, + + // violator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: () => [et.units(55).sub(ctx.stash.repay).add(ctx.stash.reserves), '0.000000000001'], }, + { call: 'eTokens.eTST.balanceOfUnderlying', args: [ctx.wallet2.address], equals: () => [et.units(50).sub(ctx.stash.yield), '0.000000000001'], }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: async (r) => { + let targetHealth = (await ctx.contracts.liquidation.TARGET_HEALTH()) / 1e18; + et.equals(r.collateralValue / r.liabilityValue, targetHealth, 0.00000001); + }}, + testDetailedLiability(ctx, 1.25), + ], +}) + + + + +.test({ + desc: "XV Liquidate self-collateral with override on self-collateral factor", + actions: ctx => [ + { action: 'setAssetConfig', tok: 'TST3', config: { collateralFactor: .5}, }, + { send: 'tokens.TST3.mint', args: [ctx.wallet2.address, et.eth(200)], }, + { from: ctx.wallet2, send: 'tokens.TST3.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { from: ctx.wallet2, send: 'eTokens.eTST3.deposit', args: [0, et.eth(30)], }, + { from: ctx.wallet2, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST3.address], }, + + + { send: 'tokens.TST6.mint', args: [ctx.wallet2.address, et.eth(100)], }, + { from: ctx.wallet2, send: 'tokens.TST6.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { from: ctx.wallet2, send: 'eTokens.eTST6.deposit', args: [0, et.eth(10)], }, + { from: ctx.wallet2, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST6.address], }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST6.address, + { + enabled: true, + collateralFactor: Math.floor(0.6 * 4e9), + }, + ], }, + + { from: ctx.wallet2, send: 'eTokens.eTST.mint', args: [0, et.eth(45)], }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST2.address, + { + enabled: true, + collateralFactor: Math.floor(0.9 * 4e9), + }, + ], }, + + + { action: 'updateUniswapPrice', pair: 'TST/WETH', price: '7.4', }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST.address, + { + enabled: true, + collateralFactor: Math.floor(0.8 * 4e9), + }, + ], }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.collateralValue / r.liabilityValue, 0.914, 0.001); + }, }, + testDetailedLiability(ctx, 0.914), + + { callStatic: 'liquidation.checkLiquidation', args: [ctx.wallet.address, ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST.address], + onResult: r => { + et.equals(r.healthScore, 0.914, 0.001); + ctx.stash.repay = r.repay; + ctx.stash.yield = r.yield; + }, + }, + + // Successful liquidation + + { call: 'eTokens.eTST.reserveBalanceUnderlying', args: [], equals: [0, '0.000000000001'] }, + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: et.eth('45'), }, + + { send: 'liquidation.liquidate', args: [ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST.address, () => ctx.stash.repay, 0], }, + + // liquidator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet.address], equals: () => ctx.stash.repay, }, + { call: 'eTokens.eTST.balanceOfUnderlying', args: [ctx.wallet.address], equals: () => [ctx.stash.yield.add(et.eth(100)), '0.00001'], }, // 100 pre-existing depsit + + // reserves: + { call: 'eTokens.eTST.reserveBalanceUnderlying', onResult: (r) => ctx.stash.reserves = r, }, + + // violator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: () => [et.units(45).sub(ctx.stash.repay).add(ctx.stash.reserves), '0.000000000001'], }, + { call: 'eTokens.eTST.balanceOfUnderlying', args: [ctx.wallet2.address], equals: () => [et.units(45).sub(ctx.stash.yield), '0.000000000001'], }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: async (r) => { + let targetHealth = (await ctx.contracts.liquidation.TARGET_HEALTH()) / 1e18; + et.equals(r.collateralValue / r.liabilityValue, targetHealth, 0.00000001); + }}, + testDetailedLiability(ctx, 1.25), + ], +}) + + + + +.test({ + desc: "XVI Liquidate self-collateral with second borrow - override deactivated", + actions: ctx => [ + { action: 'setAssetConfig', tok: 'TST3', config: { collateralFactor: .5}, }, + { send: 'tokens.TST3.mint', args: [ctx.wallet2.address, et.eth(200)], }, + { from: ctx.wallet2, send: 'tokens.TST3.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { from: ctx.wallet2, send: 'eTokens.eTST3.deposit', args: [0, et.eth(30)], }, + { from: ctx.wallet2, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST3.address], }, + + + { send: 'tokens.TST6.mint', args: [ctx.wallet2.address, et.eth(100)], }, + { from: ctx.wallet2, send: 'tokens.TST6.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { from: ctx.wallet2, send: 'eTokens.eTST6.deposit', args: [0, et.eth(10)], }, + { from: ctx.wallet2, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST6.address], }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST6.address, + { + enabled: true, + collateralFactor: Math.floor(0.6 * 4e9), + }, + ], }, + + { from: ctx.wallet2, send: 'eTokens.eTST.mint', args: [0, et.eth(4.5)], }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST2.address, + { + enabled: true, + collateralFactor: Math.floor(0.9 * 4e9), + }, + ], }, + + + // add second liability + { action: 'setAssetConfig', tok: 'TST', config: { borrowIsolated: false }, }, + { action: 'setAssetConfig', tok: 'TST2', config: { borrowIsolated: false}, }, + { from: ctx.wallet2, send: 'dTokens.dTST2.borrow', args: [0, et.eth(0.1)], }, + + { action: 'updateUniswapPrice', pair: 'TST/WETH', price: '7.4', }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.collateralValue / r.liabilityValue, 0.965, 0.001); + }, }, + testDetailedLiability(ctx, 0.965), + + { callStatic: 'liquidation.checkLiquidation', args: [ctx.wallet.address, ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST.address], + onResult: r => { + et.equals(r.healthScore, 0.965, 0.001); + ctx.stash.repay = r.repay; + ctx.stash.yield = r.yield; + }, + }, + + // Successful liquidation + + { call: 'eTokens.eTST.reserveBalanceUnderlying', args: [], equals: [0, '0.000000000001'] }, + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: et.eth('4.5'), }, + + { send: 'liquidation.liquidate', args: [ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST.address, () => ctx.stash.repay, 0], }, + + // liquidator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet.address], equals: () => ctx.stash.repay, }, + { call: 'eTokens.eTST.balanceOfUnderlying', args: [ctx.wallet.address], equals: () => [ctx.stash.yield.add(et.eth(100)), '0.00001'], }, // 100 pre-existing depsit + + // reserves: + { call: 'eTokens.eTST.reserveBalanceUnderlying', onResult: (r) => ctx.stash.reserves = r, }, + + // violator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: () => [et.units(4.5).sub(ctx.stash.repay).add(ctx.stash.reserves), '0.000000000001'], }, + { call: 'eTokens.eTST.balanceOfUnderlying', args: [ctx.wallet2.address], equals: () => [et.units(4.5).sub(ctx.stash.yield), '0.000000000001'], }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: async (r) => { + let targetHealth = (await ctx.contracts.liquidation.TARGET_HEALTH()) / 1e18; + et.equals(r.collateralValue / r.liabilityValue, targetHealth, 0.00000001); + }}, + testDetailedLiability(ctx, 1.25), + ], +}) + + + + +.test({ + desc: "XVII Liquidate self-collateral with override set to 0 on self-collateral factor", + actions: ctx => [ + { action: 'setAssetConfig', tok: 'TST3', config: { collateralFactor: .5}, }, + { send: 'tokens.TST3.mint', args: [ctx.wallet2.address, et.eth(200)], }, + { from: ctx.wallet2, send: 'tokens.TST3.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { from: ctx.wallet2, send: 'eTokens.eTST3.deposit', args: [0, et.eth(30)], }, + { from: ctx.wallet2, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST3.address], }, + + + { send: 'tokens.TST6.mint', args: [ctx.wallet2.address, et.eth(100)], }, + { from: ctx.wallet2, send: 'tokens.TST6.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { from: ctx.wallet2, send: 'eTokens.eTST6.deposit', args: [0, et.eth(10)], }, + { from: ctx.wallet2, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST6.address], }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST6.address, + { + enabled: true, + collateralFactor: Math.floor(0.6 * 4e9), + }, + ], }, + + { from: ctx.wallet2, send: 'eTokens.eTST.mint', args: [0, et.eth(45)], }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST2.address, + { + enabled: true, + collateralFactor: Math.floor(0.9 * 4e9), + }, + ], }, + + + { action: 'updateUniswapPrice', pair: 'TST/WETH', price: '7.4', }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST.address, + { + enabled: true, + collateralFactor: 0, + }, + ], }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.collateralValue / r.liabilityValue, 0.09, 0.001); + }, }, + testDetailedLiability(ctx, 0.09), + + { callStatic: 'liquidation.checkLiquidation', args: [ctx.wallet.address, ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST.address], + onResult: r => { + et.equals(r.healthScore, 0.09, 0.001); + ctx.stash.repay = r.repay; + ctx.stash.yield = r.yield; + }, + }, + + // Successful liquidation + + { call: 'eTokens.eTST.reserveBalanceUnderlying', args: [], equals: [0, '0.000000000001'] }, + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: et.eth('45'), }, + + // Liquidator must burn max, since the position is not collateralised + { action: 'sendBatch', batch: [ + { send: 'liquidation.liquidate', args: [ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST.address, () => ctx.stash.repay, 0], }, + { send: 'eTokens.eTST.burn', args: [0, et.MaxUint256]} + ], + deferLiquidityChecks: [ctx.wallet.address], + }, + + // liquidator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet.address], equals: 0, }, + { call: 'eTokens.eTST.balanceOfUnderlying', args: [ctx.wallet.address], equals: () => [ctx.stash.yield.sub(ctx.stash.repay).add(et.eth(100)), '0.00001'], }, // 100 pre-existing depsit + + // reserves: + { call: 'eTokens.eTST.reserveBalanceUnderlying', onResult: (r) => ctx.stash.reserves = r, }, + + // violator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: () => [et.units(45).sub(ctx.stash.repay).add(ctx.stash.reserves), '0.000000000001'], }, + { call: 'eTokens.eTST.balanceOfUnderlying', args: [ctx.wallet2.address], equals: () => [et.units(45).sub(ctx.stash.yield), '0.000000000001'], }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: async (r) => { + et.equals(r.collateralValue / r.liabilityValue, 0.687, 0.001); + }}, + testDetailedLiability(ctx, 0.687), + ], +}) + + + + +.test({ + desc: "XVIII Liquidate self-collateral with override set to 0 on self-collateral factor, no other overrides present", + actions: ctx => [ + { action: 'setAssetConfig', tok: 'TST3', config: { collateralFactor: .5}, }, + { send: 'tokens.TST3.mint', args: [ctx.wallet2.address, et.eth(200)], }, + { from: ctx.wallet2, send: 'tokens.TST3.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { from: ctx.wallet2, send: 'eTokens.eTST3.deposit', args: [0, et.eth(30)], }, + { from: ctx.wallet2, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST3.address], }, + + + { send: 'tokens.TST6.mint', args: [ctx.wallet2.address, et.eth(100)], }, + { from: ctx.wallet2, send: 'tokens.TST6.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { from: ctx.wallet2, send: 'eTokens.eTST6.deposit', args: [0, et.eth(10)], }, + { from: ctx.wallet2, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST6.address], }, + + { from: ctx.wallet2, send: 'eTokens.eTST.mint', args: [0, et.eth(45)], }, + + { action: 'updateUniswapPrice', pair: 'TST/WETH', price: '7.4', }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST.address, + { + enabled: true, + collateralFactor: 0, + }, + ], }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.collateralValue / r.liabilityValue, 0.066, 0.001); + }, }, + + testDetailedLiability(ctx, 0.066), + + { callStatic: 'liquidation.checkLiquidation', args: [ctx.wallet.address, ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST.address], + onResult: r => { + et.equals(r.healthScore, 0.066, 0.001); + ctx.stash.repay = r.repay; + ctx.stash.yield = r.yield; + }, + }, + + // Successful liquidation + + { call: 'eTokens.eTST.reserveBalanceUnderlying', args: [], equals: [0, '0.000000000001'] }, + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: et.eth('45'), }, + + // Liquidator must burn max, since the position is not collateralised + { action: 'sendBatch', batch: [ + { send: 'liquidation.liquidate', args: [ctx.wallet2.address, ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST.address, () => ctx.stash.repay, 0], }, + { send: 'eTokens.eTST.burn', args: [0, et.MaxUint256]} + ], + deferLiquidityChecks: [ctx.wallet.address], + }, + + // liquidator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet.address], equals: 0, }, + { call: 'eTokens.eTST.balanceOfUnderlying', args: [ctx.wallet.address], equals: () => [ctx.stash.yield.sub(ctx.stash.repay).add(et.eth(100)), '0.00001'], }, // 100 pre-existing depsit + + // reserves: + { call: 'eTokens.eTST.reserveBalanceUnderlying', onResult: (r) => ctx.stash.reserves = r, }, + + // violator: + { call: 'dTokens.dTST.balanceOf', args: [ctx.wallet2.address], equals: () => [et.units(45).sub(ctx.stash.repay).add(ctx.stash.reserves), '0.000000000001'], }, + { call: 'eTokens.eTST.balanceOfUnderlying', args: [ctx.wallet2.address], equals: () => [et.units(45).sub(ctx.stash.yield), '0.000000000001'], }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: async (r) => { + et.equals(r.collateralValue / r.liabilityValue, 0.333, 0.001); + }}, + testDetailedLiability(ctx, 0.333), + ], +}) + +.run(); diff --git a/test/liquidity.js b/test/liquidity.js index 50e239e8..352d2a3b 100644 --- a/test/liquidity.js +++ b/test/liquidity.js @@ -23,7 +23,7 @@ et.testSet({ .test({ desc: "simple liquidity", actions: ctx => [ - { call: 'exec.detailedLiquidity', args: [ctx.wallet.address], onResult: r => { + { call: 'exec.liquidityPerAsset', args: [ctx.wallet.address], onResult: r => { et.equals(r[0].status.collateralValue, 10 * 2 * .75, .002); // amount * price * collateralFactor = 15 et.equals(r[0].status.liabilityValue, 0); @@ -31,7 +31,7 @@ et.testSet({ et.equals(r[1].status.liabilityValue, 0); }, }, - { call: 'exec.detailedLiquidity', args: [ctx.wallet2.address], onResult: r => { + { call: 'exec.liquidityPerAsset', args: [ctx.wallet2.address], onResult: r => { et.equals(r[0].status.collateralValue, 0); et.equals(r[0].status.liabilityValue, 0); @@ -41,7 +41,7 @@ et.testSet({ { from: ctx.wallet2, send: 'dTokens.dTST.borrow', args: [0, et.eth(.1)], }, - { call: 'exec.detailedLiquidity', args: [ctx.wallet2.address], onResult: r => { + { call: 'exec.liquidityPerAsset', args: [ctx.wallet2.address], onResult: r => { et.equals(r[0].status.collateralValue, 0); et.equals(r[0].status.liabilityValue, 0.1 * 2 / .4, 0.0001); // 0.5 @@ -63,7 +63,7 @@ et.testSet({ { from: ctx.wallet2, send: 'dTokens.dTST.borrow', args: [0, et.eth(0.0244)], }, - { call: 'exec.detailedLiquidity', args: [ctx.wallet2.address], onResult: r => { + { call: 'exec.liquidityPerAsset', args: [ctx.wallet2.address], onResult: r => { et.equals(r[0].status.collateralValue, 0); et.equals(r[0].status.liabilityValue, (.1 + 0.0244) * 2 / .4, 0.0001); @@ -93,7 +93,7 @@ et.testSet({ { from: ctx.wallet2, send: 'eTokens.eTST2.transfer', args: [ctx.wallet3.address, et.eth('1.967')], }, - { call: 'exec.detailedLiquidity', args: [ctx.wallet2.address], onResult: r => { + { call: 'exec.liquidityPerAsset', args: [ctx.wallet2.address], onResult: r => { et.equals(r[0].status.liabilityValue, 0.5, 0.001); et.equals(r[1].status.collateralValue, 0.5, 0.001); }, }, @@ -110,7 +110,7 @@ et.testSet({ actions: ctx => [ { from: ctx.wallet2, send: 'dTokens.dTST.borrow', args: [0, et.eth(.1)], }, - { call: 'exec.detailedLiquidity', args: [ctx.wallet2.address], onResult: r => { + { call: 'exec.liquidityPerAsset', args: [ctx.wallet2.address], onResult: r => { et.equals(r[0].status.liabilityValue, 0.5, 0.0001); }, }, @@ -119,7 +119,7 @@ et.testSet({ { from: ctx.wallet3, send: 'eTokens.eTST2.deposit', args: [0, et.eth(6)], }, - { call: 'exec.detailedLiquidity', args: [ctx.wallet3.address], onResult: r => { + { call: 'exec.liquidityPerAsset', args: [ctx.wallet3.address], onResult: r => { et.equals(r[1].status.collateralValue, 6 * 0.083 * .75, 0.0001); }, }, @@ -137,11 +137,11 @@ et.testSet({ { from: ctx.wallet2, send: 'dTokens.dTST.transfer', args: [ctx.wallet3.address, et.eth('.0746')], }, - { call: 'exec.detailedLiquidity', args: [ctx.wallet3.address], onResult: r => { + { call: 'exec.liquidityPerAsset', args: [ctx.wallet3.address], onResult: r => { et.equals(r[0].status.liabilityValue, 0.3735, 0.01); }, }, - { call: 'exec.detailedLiquidity', args: [ctx.wallet2.address], onResult: r => { + { call: 'exec.liquidityPerAsset', args: [ctx.wallet2.address], onResult: r => { et.equals(r[0].status.liabilityValue, 0.5 - 0.3735, 0.01); }, }, ], diff --git a/test/override.js b/test/override.js index ded7d535..b6329bfb 100644 --- a/test/override.js +++ b/test/override.js @@ -31,7 +31,7 @@ et.testSet({ { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { et.equals(r.liabilityValue, 0.5, .001); // 0.1 * 2 / 0.4 et.equals(r.collateralValue, 3.75, .001); // 10 * 0.5 * 0.75 - et.expect(r.overrideEnabled).to.equal(false); + et.assert(r.overrideCollateralValue.eq(0)); }, }, // Override is added for this liability/collateral pair @@ -48,7 +48,7 @@ et.testSet({ { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { et.equals(r.liabilityValue, 0.2, .001); // 0.1 * 2 et.equals(r.collateralValue, 4.85, .001); // 10 * 0.5 * 0.97 - et.expect(r.overrideEnabled).to.equal(true); + et.assert(r.overrideCollateralValue.gt(0)); }, }, { from: ctx.wallet2, send: 'dTokens.dTST3.borrow', args: [0, et.eth(.1)], }, @@ -58,12 +58,239 @@ et.testSet({ { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { et.equals(r.liabilityValue, 0.55, .001); // (0.1 * 2 / 0.4) + (0.1 * 0.25 / 0.5) et.equals(r.collateralValue, 3.75, .001); // 10 * 0.5 * 0.75 - et.expect(r.overrideEnabled).to.equal(false); + et.assert(r.overrideCollateralValue.eq(0)); }, }, + + { from: ctx.wallet2, send: 'tokens.TST3.approve', args: [ctx.contracts.euler.address, et.MaxUint256], }, + { from: ctx.wallet2, send: 'dTokens.dTST3.repay', args: [0, et.MaxUint256], }, + + // Override is enabled after repay + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.liabilityValue, 0.2, .001); // 0.1 * 2 + et.equals(r.collateralValue, 4.85, .001); // 10 * 0.5 * 0.97 + et.assert(r.overrideCollateralValue.gt(0)); + }, }, + ], +}) + + + +.test({ + desc: "override on non-collateral asset", + actions: ctx => [ + // set collateral factor to 0 + { action: 'setAssetConfig', tok: 'TST2', config: { collateralFactor: 0, }, }, + + { from: ctx.wallet2, send: 'dTokens.dTST.borrow', args: [0, et.eth(.1)], expectError: 'e/collateral-violation' }, + + // Override is added for this liability/collateral pair + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST2.address, + { + enabled: true, + collateralFactor: Math.floor(0.97 * 4e9), + }, + ], }, + + // Borrow is possible now + + { from: ctx.wallet2, send: 'dTokens.dTST.borrow', args: [0, et.eth(.1)], }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.liabilityValue, 0.2, .001); // 0.1 * 2 + et.equals(r.collateralValue, 4.85, .001); // 10 * 0.5 * 0.97 + et.assert(r.overrideCollateralValue.gt(0)); + }, }, + + // Additional borrow on account is not permitted as it disables override + + { from: ctx.wallet2, send: 'dTokens.dTST3.borrow', args: [0, et.eth(.1)], expectError: 'e/collateral-violation' }, + + // Self-collateralisation is permitted + + { from: ctx.wallet2, send: 'eTokens.eTST.mint', args: [0, et.eth(.001)] }, + ], }) +.test({ + desc: "override self-collateral factor", + actions: ctx => [ + // set collateral factor to 0 + + { from: ctx.wallet2, send: 'eTokens.eTST2.mint', args: [0, et.eth(10)], }, + + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.liabilityValue, 5, .001); // 10 * 0.5 (price) / 1 (BF) + et.equals(r.collateralValue, 9.5, .001); // 20 * 0.5 (price) * 0.95 (SCF) + et.equals(r.overrideCollateralValue, 9.5, .001); // whole collateral is in override + }, }, + + // Override is added for the self collateralisation + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST2.address, + ctx.contracts.tokens.TST2.address, + { + enabled: true, + collateralFactor: Math.floor(0.8 * 4e9), + }, + ], }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.liabilityValue, 5, .001); // 10 * 0.5 (price) / 1 (BF) + et.equals(r.collateralValue, 8, .001); // 20 * 0.5 (price) * 0.8 (CF) + et.equals(r.overrideCollateralValue, 8, .001); // whole collateral is in override + }, }, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST2.address, + ctx.contracts.tokens.TST2.address, + { + enabled: false, + collateralFactor:0, + }, + ], }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.liabilityValue, 5, .001); // 10 * 0.5 (price) / 1 (BF) + et.equals(r.collateralValue, 9.5, .001); // 20 * 0.5 (price) * 0.95 (SCF) + et.equals(r.overrideCollateralValue, 9.5, .001); // whole collateral is in override + }, }, + ], +}) + + + +.test({ + desc: "self-collateral override doesn't apply with multiple borrows", + actions: ctx => [ + { action: 'setAssetConfig', tok: 'TST2', config: { borrowIsolated: false, collateralFactor: .7, borrowFactor: .6, }, }, + + { from: ctx.wallet2, send: 'eTokens.eTST2.mint', args: [0, et.eth(1)], }, + + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.liabilityValue, .5, .001); // 1 * 0.5 (price) / 1 (BF) + et.equals(r.collateralValue, 5.225, .001); // 11 * 0.5 (price) * 0.95 (SCF) + et.equals(r.overrideCollateralValue, 5.225, .001); // whole collateral is in override + }, }, + + // second borrow + { from: ctx.wallet2, send: 'dTokens.dTST3.borrow', args: [0, et.eth(2)], }, + + // all values counted with regular factors + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.liabilityValue, 1.833, .001); // 1 * 0.5 (price) / .6 (BF) + 2 * 0.25 (price) / 0.5 (BF) + et.equals(r.collateralValue, 3.85, .001); // 11 * 0.5 (price) * 0.7 (CF) + et.equals(r.overrideCollateralValue, 0); // no overrides + }, }, + + ], +}) + + + +.test({ + desc: "override getters", + actions: ctx => [ + { call: 'markets.getOverride', args: [ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST2.address,], onResult: r => { + et.expect(r.enabled).to.equal(false); + et.expect(r.collateralFactor).to.equal(0); + }}, + { call: 'markets.getOverrideCollaterals', args: [ctx.contracts.tokens.TST.address], onResult: r => { + et.expect(r.length).to.equal(0); + }}, + { call: 'markets.getOverrideLiabilities', args: [ctx.contracts.tokens.TST2.address], onResult: r => { + et.expect(r.length).to.equal(0); + }}, + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST2.address, + { + enabled: true, + collateralFactor: Math.floor(0.97 * 4e9), + }, + ], }, + + { call: 'markets.getOverride', args: [ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST2.address,], onResult: r => { + et.expect(r.enabled).to.equal(true); + et.expect(r.collateralFactor).to.equal(0.97 * 4e9); + }}, + { call: 'markets.getOverrideCollaterals', args: [ctx.contracts.tokens.TST.address], onResult: r => { + et.expect(r.length).to.equal(1); + et.expect(r[0]).to.equal(ctx.contracts.tokens.TST2.address); + }}, + { call: 'markets.getOverrideLiabilities', args: [ctx.contracts.tokens.TST2.address], onResult: r => { + et.expect(r.length).to.equal(1); + et.expect(r[0]).to.equal(ctx.contracts.tokens.TST.address); + }}, + + // no duplicates + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST2.address, + { + enabled: true, + collateralFactor: Math.floor(0.5 * 4e9), + }, + ], }, + + { call: 'markets.getOverride', args: [ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST2.address,], onResult: r => { + et.expect(r.enabled).to.equal(true); + et.expect(r.collateralFactor).to.equal(0.5 * 4e9); + }}, + { call: 'markets.getOverrideCollaterals', args: [ctx.contracts.tokens.TST.address], onResult: r => { + et.expect(r.length).to.equal(1); + et.expect(r[0]).to.equal(ctx.contracts.tokens.TST2.address); + }}, + { call: 'markets.getOverrideLiabilities', args: [ctx.contracts.tokens.TST2.address], onResult: r => { + et.expect(r.length).to.equal(1); + et.expect(r[0]).to.equal(ctx.contracts.tokens.TST.address); + }}, + + // disabling removes from array + + // add one more override for TST as liability + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST3.address, + { + enabled: true, + collateralFactor: Math.floor(0.6 * 4e9), + }, + ], }, + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST2.address, + { + enabled: false, + collateralFactor: Math.floor(0.6 * 4e9), + }, + ], }, + + { call: 'markets.getOverride', args: [ctx.contracts.tokens.TST.address, ctx.contracts.tokens.TST2.address,], onResult: r => { + et.expect(r.enabled).to.equal(false); + et.expect(r.collateralFactor).to.equal(0.6 * 4e9); + }}, + { call: 'markets.getOverrideCollaterals', args: [ctx.contracts.tokens.TST.address], onResult: r => { + et.expect(r.length).to.equal(1); + et.expect(r[0]).to.equal(ctx.contracts.tokens.TST3.address); + }}, + { call: 'markets.getOverrideLiabilities', args: [ctx.contracts.tokens.TST2.address], onResult: r => { + et.expect(r.length).to.equal(0); + }}, + ], +}) + + .run(); diff --git a/test/pToken.js b/test/pToken.js index 9a567071..9e7ed01e 100644 --- a/test/pToken.js +++ b/test/pToken.js @@ -75,7 +75,7 @@ et.testSet({ { send: 'eTokens.epTST.deposit', args: [0, et.eth(5)], }, - { call: 'exec.detailedLiquidity', args: [ctx.wallet.address], onResult: r => { + { call: 'exec.liquidityPerAsset', args: [ctx.wallet.address], onResult: r => { et.equals(r[0].status.collateralValue, 3.75, 0.001); }, }, @@ -110,7 +110,7 @@ et.testSet({ { send: 'markets.enterMarket', args: [0, ctx.contracts.pTokens.pTST.address], }, ]}, - { call: 'exec.detailedLiquidity', args: [ctx.wallet.address], onResult: r => { + { call: 'exec.liquidityPerAsset', args: [ctx.wallet.address], onResult: r => { et.equals(r[0].status.collateralValue, 3.75, 0.001); }, }, diff --git a/test/reservesInitial.js b/test/reservesInitial.js index fccbdf1a..d4815d0e 100644 --- a/test/reservesInitial.js +++ b/test/reservesInitial.js @@ -157,4 +157,26 @@ et.testSet({ + +.test({ + desc: "market activation with pre-existing euler balance", + actions: ctx => [ + { send: 'tokens.TST.mint', args: [ctx.contracts.euler.address, et.eth(100)] }, + { send: 'markets.activateMarket', args: [ctx.contracts.tokens.TST.address], }, + + // all pre-existing balance is attributed to the reserves + { call: 'eTokens.eTST.reserveBalanceUnderlying', equals: et.eth(100) }, + + // first depositor only owns their deposit + { call: 'tokens.TST.balanceOf', args: [ctx.wallet.address], equals: [et.eth(100)], }, + + { send: 'eTokens.eTST.deposit', args: [0, et.eth(1)], }, + { send: 'eTokens.eTST.withdraw', args: [0, et.MaxUint256], }, + { call: 'tokens.TST.balanceOf', args: [ctx.wallet.address], equals: [et.eth(100)], }, + + { call: 'eTokens.eTST.reserveBalanceUnderlying', equals: et.eth(100) }, + ], +}) + + .run(); diff --git a/test/selfCollateralisation.js b/test/selfCollateralisation.js index 44577127..a7c9363a 100644 --- a/test/selfCollateralisation.js +++ b/test/selfCollateralisation.js @@ -9,6 +9,7 @@ let ts = et.testSet({ ...scenarios.basicLiquidity()(ctx), { action: 'updateUniswapPrice', pair: 'TST/WETH', price: '1', }, + { action: 'updateUniswapPrice', pair: 'TST2/WETH', price: '1', }, { action: 'updateUniswapPrice', pair: 'TST3/WETH', price: '1', }, { action: 'setAssetConfig', tok: 'TST3', config: { borrowFactor: .6}, }, @@ -52,11 +53,13 @@ let ts = et.testSet({ // mint subtracted default reserve to makeup 4.5 { from: ctx.wallet3, send: 'eTokens.eTST3.mint', args: [0, et.BN(et.DefaultReserve)], }, - { callStatic: 'exec.detailedLiquidity', args: [ctx.wallet3.address], onResult: r => { + { callStatic: 'exec.liquidityPerAsset', args: [ctx.wallet3.address], onResult: r => { // Balance adjusted down by SELF_COLLATERAL_FACTOR et.equals(r[2].status.collateralValue, 4.275, 0.001); // 4.5 * 0.95 // Remaining liability is adjusted up by asset borrow factor et.equals(r[2].status.liabilityValue, 4.65, 0.001); // 4.275 + ((4.5 - 4.275) / .6) + // Self-collateral counts as implicit override + et.equals(r[2].status.overrideCollateralValue, 4.275, 0.001); }}, { from: ctx.wallet3, send: 'dTokens.dTST2.borrow', args: [0, et.eth(0.001)], expectError: 'e/borrow-isolation-violation' }, @@ -70,27 +73,13 @@ let ts = et.testSet({ { from: ctx.wallet3, send: 'eTokens.eTST3.deposit', args: [0, et.eth(3)], }, - // This does not effect the user's collateral value because CF == 0: + // This extra supply counts to collateral value at CF = 0.95 - { callStatic: 'exec.detailedLiquidity', args: [ctx.wallet3.address], onResult: r => { - et.equals(r[2].status.collateralValue, 4.5); // Limited to liability because TST3 has 0 collateral factor + { callStatic: 'exec.liquidityPerAsset', args: [ctx.wallet3.address], onResult: r => { + et.equals(r[2].status.collateralValue, 4.275 + 3 * 0.95, 0.001); et.equals(r[2].status.liabilityValue, 4.5); // Full liability is now self-collateralised }}, - // Now we give TST3 a collateral factor of 0.7: - - { action: 'setAssetConfig', tok: 'TST3', config: { collateralFactor: 0.7, }, }, - - { callStatic: 'exec.detailedLiquidity', args: [ctx.wallet3.address], onResult: r => { - // The liability is fully self-collateralised as before, with 4.5. - // However, there is also extra collateral available: 7.5 - (4.5/.95) - // This extra collateral is available for other borrows, after adjusting - // down according to the asset's collateral factor of 0.7. - - et.equals(r[2].status.collateralValue, '6.434210526315789474', 0.001); // 4.5 + ((7.5 - (4.5/.95)) * .7) - et.equals(r[2].status.liabilityValue, 4.5); // unchanged - }}, - { action: 'revert', }, @@ -103,7 +92,7 @@ let ts = et.testSet({ { from: ctx.wallet3, send: 'eTokens.eTST.deposit', args: [0, et.eth(10)], }, // extra collateral so borrow succeeds { from: ctx.wallet3, send: 'dTokens.dTST3.borrow', args: [0, et.eth(3)], }, - { callStatic: 'exec.detailedLiquidity', args: [ctx.wallet3.address], onResult: r => { + { callStatic: 'exec.liquidityPerAsset', args: [ctx.wallet3.address], onResult: r => { // 4.5*.95=4.275 of the liability is self-collateralised. This leaves .225 of the original 4.5 // mint and 3 of the new borrow as unmet liabilities, which are adjusted up according // to the borrow factor of .6. @@ -116,6 +105,70 @@ let ts = et.testSet({ + +.test({ + desc: "self collateralisation only activates with a single borrow", + actions: ctx => [ + { from: ctx.wallet3, send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST.address], }, + { from: ctx.wallet3, send: 'eTokens.eTST.deposit', args: [0, et.eth(0.5)], }, + { action: 'setAssetConfig', tok: 'TST2', config: { borrowIsolated: false, collateralFactor: 0.6 }, }, + { action: 'setAssetConfig', tok: 'TST3', config: { borrowIsolated: false, collateralFactor: 0.5 }, }, + + + // self-collateral is activated + { from: ctx.wallet3, send: 'eTokens.eTST3.mint', args: [0, et.eth(0.1)], }, + + { callStatic: 'exec.liquidityPerAsset', args: [ctx.wallet3.address], onResult: r => { + et.equals(r[2].status.collateralValue, 0.095, 0.001); // 0.1 * 0.95 + // Remaining liability is adjusted up by asset borrow factor + et.equals(r[2].status.liabilityValue, 0.1033, 0.001); // 0.095 + ((1 - 0.95) / .6) + // Self-collateral counts as implicit override + et.equals(r[2].status.overrideCollateralValue, 0.095, 0.001); + }}, + + { action: 'snapshot', }, + + // self-collateral is deactivated + { from: ctx.wallet3, send: 'dTokens.dTST2.borrow', args: [0, et.eth(0.001)] }, + + { callStatic: 'exec.liquidityPerAsset', args: [ctx.wallet3.address], onResult: r => { + // Collateral is counted at a regular CF = 0.5 + et.equals(r[2].status.collateralValue, 0.05, 0.001); // 0.1 * 0.5 + // Liability is counted at a regular BF = 0.6 + et.equals(r[2].status.liabilityValue, 0.166, 0.001); // 0.1 / 0.6 + // Override is not active + et.equals(r[2].status.overrideCollateralValue, 0.0); + }}, + + { action: 'revert', }, + + // multiple mints are possible + { from: ctx.wallet3, send: 'eTokens.eTST2.mint', args: [0, et.eth(0.1)] }, + + // TST3 self-collateral is deactivated + { callStatic: 'exec.liquidityPerAsset', args: [ctx.wallet3.address], onResult: r => { + // Collateral is counted at a regular CF = 0.5 + et.equals(r[2].status.collateralValue, 0.05, 0.001); // 0.1 * 0.6 + // Liability is counted at a regular BF = 0.6 + et.equals(r[2].status.liabilityValue, 0.166, 0.001); // 0.1 / 0.6 + // Override is not active + et.equals(r[2].status.overrideCollateralValue, 0.0); + }}, + + // TST2 mint doesn't self-collateralise + { callStatic: 'exec.liquidityPerAsset', args: [ctx.wallet3.address], onResult: r => { + // Collateral is counted at a regular CF = 0.6 + et.equals(r[1].status.collateralValue, 0.06, 0.001); // 0.1 * 0.6 + // Liability is counted at a regular BF = 0.4 + et.equals(r[1].status.liabilityValue, 0.25, 0.001); // 0.1 / 0.4 + // Override is not active + et.equals(r[1].status.overrideCollateralValue, 0.0); + }}, + ], +}) + + + .test({ desc: "liquidation, topped-up with other collateral", actions: ctx => [ @@ -159,13 +212,59 @@ let ts = et.testSet({ { callStatic: 'liquidation.checkLiquidation', args: [ctx.wallet.address, ctx.wallet3.address, ctx.contracts.tokens.TST3.address, ctx.contracts.tokens.TST3.address], onResult: r => { - et.equals(r.healthScore, 2.024, 0.001); + et.equals(r.healthScore, 1.25, 0.001); + }, + }, + + { call: 'eTokens.eTST.balanceOfUnderlying', args: [ctx.wallet3.address], equals: [0.5, '0.000000001'], }, + { call: 'eTokens.eTST3.balanceOfUnderlying', args: [ctx.wallet3.address], equals: [0.487, '.001'], }, + { call: 'dTokens.dTST3.balanceOf', args: [ctx.wallet3.address], equals: [.587, .001], }, + + { action: 'revert', }, + + // Liquidate the self collateral with override + + { action: 'snapshot', }, + + { from: ctx.wallet3, send: 'markets.exitMarket', args: [0, ctx.contracts.tokens.TST2.address], }, + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST3.address, + ctx.contracts.tokens.TST.address, + { + enabled: true, + collateralFactor: Math.floor(0.4 * 4e9), + }, + ], }, + + { call: 'exec.liquidity', args: [ctx.wallet3.address], onResult: r => { + et.equals(r.collateralValue, 4.475, 0.001); + et.equals(r.liabilityValue, 4.53, 0.001); + et.equals(r.overrideCollateralValue, 4.475, 0.001); + }, }, + + { callStatic: 'liquidation.checkLiquidation', args: [ctx.wallet.address, ctx.wallet3.address, ctx.contracts.tokens.TST3.address, ctx.contracts.tokens.TST3.address], + onResult: r => { + et.equals(r.healthScore, 4.4755/4.5307, 0.0001); + ctx.stash.repay = r.repay; + ctx.stash.yield = r.yield; + } + }, + + { call: 'eTokens.eTST.balanceOfUnderlying', args: [ctx.wallet3.address], equals: [0.5, '0.000000001'], }, + { call: 'eTokens.eTST3.balanceOfUnderlying', args: [ctx.wallet3.address], equals: ['4.5005', '.0001'], }, + { call: 'dTokens.dTST3.balanceOf', args: [ctx.wallet3.address], equals: ['4.5086', '.0001'], }, + + { send: 'liquidation.liquidate', args: [ctx.wallet3.address, ctx.contracts.tokens.TST3.address, ctx.contracts.tokens.TST3.address, () => ctx.stash.repay, 0], }, + + { callStatic: 'liquidation.checkLiquidation', args: [ctx.wallet.address, ctx.wallet3.address, ctx.contracts.tokens.TST3.address, ctx.contracts.tokens.TST3.address], + onResult: r => { + et.equals(r.healthScore, 1.25, 0.001); }, }, { call: 'eTokens.eTST.balanceOfUnderlying', args: [ctx.wallet3.address], equals: [0.5, '0.000000001'], }, - { call: 'eTokens.eTST3.balanceOfUnderlying', args: [ctx.wallet3.address], equals: [0, '.00000000001'], }, - { call: 'dTokens.dTST3.balanceOf', args: [ctx.wallet3.address], equals: [.111, .001], }, + { call: 'eTokens.eTST3.balanceOfUnderlying', args: [ctx.wallet3.address], equals: [0.034, '.001'], }, + { call: 'dTokens.dTST3.balanceOf', args: [ctx.wallet3.address], equals: [.185, .001], }, { action: 'revert', }, @@ -184,11 +283,11 @@ let ts = et.testSet({ { call: 'eTokens.eTST3.balanceOfUnderlying', args: [ctx.wallet3.address], equals: [4.500, .001], }, { send: 'liquidation.liquidate', args: [ctx.wallet3.address, ctx.contracts.tokens.TST3.address, ctx.contracts.tokens.TST.address, () => ctx.stash.repay, 0], }, - // Health score is exactly 1 because all TST collateral has been consumed, and the remainder is fully self-collateralised + // Health score is above 1 because all TST collateral has been consumed, and the extra remaining TST3 counts towards collateral value { callStatic: 'liquidation.checkLiquidation', args: [ctx.wallet.address, ctx.wallet3.address, ctx.contracts.tokens.TST3.address, ctx.contracts.tokens.TST.address], onResult: r => { - et.equals(r.healthScore, 1); + et.equals(r.healthScore, 1.063, 0.001); } }, @@ -209,7 +308,7 @@ let ts = et.testSet({ { from: ctx.wallet3, send: 'eTokens.eTST3.mint', args: [0, et.eth(4.5)], }, { callStatic: 'exec.liquidity', args: [ctx.wallet3.address], onResult: r => { - et.equals(r.collateralValue, 4.5); + et.equals(r.collateralValue, 4.75, 0.01); et.equals(r.liabilityValue, 4.5); }}, @@ -241,11 +340,11 @@ let ts = et.testSet({ { callStatic: 'liquidation.checkLiquidation', args: [ctx.wallet.address, ctx.wallet3.address, ctx.contracts.tokens.TST3.address, ctx.contracts.tokens.TST3.address], onResult: r => { - et.equals(r.healthScore, et.MaxUint256); + et.equals(r.healthScore, 1.25, 0.001); }}, - { call: 'eTokens.eTST3.balanceOfUnderlying', args: [ctx.wallet3.address], equals: ['0.1064', '.0001'], }, - { call: 'dTokens.dTST3.balanceOf', args: [ctx.wallet3.address], equals: 0, }, + { call: 'eTokens.eTST3.balanceOfUnderlying', args: [ctx.wallet3.address], equals: ['0.4843', '.0001'], }, + { call: 'dTokens.dTST3.balanceOf', args: [ctx.wallet3.address], equals: ['0.3681', '0.0001'], }, ], }); diff --git a/test/swapHubUni3.js b/test/swapHubUni3.js index a004f7dd..ecdba156 100644 --- a/test/swapHubUni3.js +++ b/test/swapHubUni3.js @@ -282,26 +282,6 @@ et.testSet({ }) -.test({ - desc: 'uni exact input single - between subaccounts, check liquidity of sub-account out', - actions: ctx => [ - ...deposit(ctx, 'TST', ctx.wallet, 1), - - // Set up borrows. Swap to WETH will result in borrow-isolation error due to self-collateralisation - ...deposit(ctx, 'TST2', ctx.wallet2), - ...deposit(ctx, 'WETH', ctx.wallet2), - ...deposit(ctx, 'TST', ctx.wallet, 2), - { action: 'setAssetConfig', tok: 'TST', config: { collateralFactor: .9}, }, - { send: 'markets.enterMarket', args: [2, ctx.contracts.tokens.TST.address] }, - { send: 'dTokens.dTST2.borrow', args: [2, 1] }, - { send: 'dTokens.dWETH.borrow', args: [2, 1] }, - - { send: 'swapHub.swap', args: [1, 2, ctx.contracts.swapHandlers.swapHandlerUniswapV3.address, basicSingleParams(ctx)], - expectError: 'e/borrow-isolation-violation', - }, - ], -}) - .test({ desc: 'uni exact input single - interest rate updated', @@ -626,28 +606,6 @@ et.testSet({ }) -.test({ - desc: 'uni exact input single - borrow isolation violation', - actions: ctx => [ - ...deposit(ctx, 'WETH'), - ...deposit(ctx, 'TST', ctx.wallet3), - ...deposit(ctx, 'TST2', ctx.wallet3), - - { send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.WETH.address] }, - - { send: 'dTokens.dTST.borrow', args: [0, et.eth(1)] }, - { send: 'dTokens.dTST2.borrow', args: [0, et.eth(1)] }, - - // TST2 deposit creates a self-collateralized loan, when regular TST loan also exists - { send: 'swapHub.swap', args: [0, 0, ctx.contracts.swapHandlers.swapHandlerUniswapV3.address, - basicSingleParams(ctx, { - underlyingIn: ctx.contracts.tokens.WETH.address, - underlyingOut: ctx.contracts.tokens.TST2.address, - }), - ], expectError: 'e/borrow-isolation-violation' }, - ], -}) - .test({ desc: 'uni exact input single - leverage in a batch', diff --git a/test/swapUni3.js b/test/swapUni3.js index 83e253e9..93e0aa72 100644 --- a/test/swapUni3.js +++ b/test/swapUni3.js @@ -530,28 +530,6 @@ et.testSet({ }) -.test({ - desc: 'uni exact input single - borrow isolation violation', - actions: ctx => [ - ...deposit(ctx, 'WETH'), - ...deposit(ctx, 'TST', ctx.wallet3), - ...deposit(ctx, 'TST2', ctx.wallet3), - - { send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.WETH.address] }, - - { send: 'dTokens.dTST.borrow', args: [0, et.eth(1)] }, - { send: 'dTokens.dTST2.borrow', args: [0, et.eth(1)] }, - - // TST2 deposit creates a self-collateralized loan, when regular TST loan also exists - { send: 'swap.swapUniExactInputSingle', args: [{ - ...basicExactInputSingleParams(ctx), - underlyingIn: ctx.contracts.tokens.WETH.address, - underlyingOut: ctx.contracts.tokens.TST2.address, - }], expectError: 'e/borrow-isolation-violation' }, - ], -}) - - .test({ desc: 'uni exact input single - leverage in a batch', actions: ctx => [ diff --git a/test/view.js b/test/view.js index 032ba01f..06cf4a49 100644 --- a/test/view.js +++ b/test/view.js @@ -18,7 +18,7 @@ et.testSet({ { send: 'dTokens.dTST2.borrow', args: [0, et.eth(5)], }, - { call: 'eulerGeneralView.doQuery', args: [{ eulerContract: ctx.contracts.euler.address, account: ctx.wallet.address, markets: [], }], assertResult: r => { + { call: 'eulerLensV1.doQuery', args: [{ eulerContract: ctx.contracts.euler.address, account: ctx.wallet.address, markets: [], }], assertResult: r => { let [tst, tst2] = r.markets; et.expect(tst.symbol).to.equal('TST'); et.expect(tst2.symbol).to.equal('TST2'); @@ -33,7 +33,7 @@ et.testSet({ { action: 'setReserveFee', underlying: 'TST2', fee: 0.06, }, - { call: 'eulerGeneralView.doQuery', args: [{ eulerContract: ctx.contracts.euler.address, account: ctx.wallet.address, markets: [], }], assertResult: r => { + { call: 'eulerLensV1.doQuery', args: [{ eulerContract: ctx.contracts.euler.address, account: ctx.wallet.address, markets: [], }], assertResult: r => { let [tst, tst2] = r.markets; et.expect(tst.symbol).to.equal('TST'); et.expect(tst2.symbol).to.equal('TST2'); @@ -48,7 +48,7 @@ et.testSet({ et.equals(tst2.supplyAPY.div(1e9), 0.048154, 0.000001); // exp(0.100066 / 2 * (1 - 0.06)) = 1.048154522328655174 }, }, - { call: 'eulerGeneralView.doQueryAccountLiquidity', args: [ctx.contracts.euler.address, [ctx.wallet.address, ctx.wallet2.address]], onResult: r => { + { call: 'eulerLensV1.doQueryAccountLiquidity', args: [ctx.contracts.euler.address, [ctx.wallet.address, ctx.wallet2.address]], onResult: r => { et.expect(r.length).to.equal(2); et.expect(r[0].markets.length).to.equal(2); et.expect(r[1].markets.length).to.equal(2); @@ -66,11 +66,11 @@ et.testSet({ { send: 'dTokens.dTST2.borrow', args: [0, et.eth(5)], }, - { call: 'eulerGeneralView.doQuery', args: [{ eulerContract: ctx.contracts.euler.address, account: ctx.wallet.address, markets: [], }], assertResult: r => { + { call: 'eulerLensV1.doQuery', args: [{ eulerContract: ctx.contracts.euler.address, account: ctx.wallet.address, markets: [], }], assertResult: r => { ctx.stash.r = r }, }, - { call: 'eulerGeneralView.doQueryBatch', args: [ + { call: 'eulerLensV1.doQueryBatch', args: [ Array(2).fill({ eulerContract: ctx.contracts.euler.address, account: ctx.wallet.address, markets: [], }) ], assertResult: r => { et.expect(r[0]).to.deep.equal(ctx.stash.r); @@ -85,7 +85,7 @@ et.testSet({ .test({ desc: "inactive market", actions: ctx => [ - { call: 'eulerGeneralView.doQuery', args: [{ eulerContract: ctx.contracts.euler.address, account: ctx.wallet.address, markets: [ctx.contracts.tokens.TST4.address], }], assertResult: r => { + { call: 'eulerLensV1.doQuery', args: [{ eulerContract: ctx.contracts.euler.address, account: ctx.wallet.address, markets: [ctx.contracts.tokens.TST4.address], }], assertResult: r => { let tst4 = r.markets[2]; et.expect(tst4.symbol).to.equal('TST4'); et.expect(tst4.eTokenAddr).to.equal(et.AddressZero) @@ -100,7 +100,7 @@ et.testSet({ .test({ desc: "address zero", actions: ctx => [ - { call: 'eulerGeneralView.doQuery', args: [{ eulerContract: ctx.contracts.euler.address, account: et.AddressZero, markets: [ctx.contracts.tokens.TST.address], }], assertResult: r => { + { call: 'eulerLensV1.doQuery', args: [{ eulerContract: ctx.contracts.euler.address, account: et.AddressZero, markets: [ctx.contracts.tokens.TST.address], }], assertResult: r => { et.expect(r.enteredMarkets).to.eql([]); }, }, ], @@ -112,7 +112,7 @@ et.testSet({ desc: "query IRM", actions: ctx => [ { action: 'setIRM', underlying: 'TST', irm: 'IRM_DEFAULT', }, - { call: 'eulerGeneralView.doQueryIRM', args: [{ eulerContract: ctx.contracts.euler.address, underlying: ctx.contracts.tokens.TST.address, }], assertResult: r => { + { call: 'eulerLensV1.doQueryIRM', args: [{ eulerContract: ctx.contracts.euler.address, underlying: ctx.contracts.tokens.TST.address, }], assertResult: r => { et.assert(r.kinkAPY.gt(r.baseAPY)); et.assert(r.maxAPY.gt(r.kinkAPY)); @@ -131,7 +131,7 @@ et.testSet({ actions: ctx => [ { send: 'tokens.TST.configure', args: ['name/return-bytes32', []], }, { send: 'tokens.TST.configure', args: ['symbol/return-bytes32', []], }, - { call: 'eulerGeneralView.doQuery', args: [{ eulerContract: ctx.contracts.euler.address, account: et.AddressZero, markets: [ctx.contracts.tokens.TST.address], }], assertResult: r => { + { call: 'eulerLensV1.doQuery', args: [{ eulerContract: ctx.contracts.euler.address, account: et.AddressZero, markets: [ctx.contracts.tokens.TST.address], }], assertResult: r => { et.expect(r.markets[0].name).to.include('Test Token'); et.expect(r.markets[0].symbol).to.include('TST'); }, },