Skip to content
This repository has been archived by the owner on Aug 26, 2024. It is now read-only.

Feat/pyth integration #66

Merged
merged 12 commits into from
Jul 10, 2024
6 changes: 6 additions & 0 deletions contracts/ILiquidator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,10 @@ interface ILiquidator {

function _whitelistRedemptionStrategies(IRedemptionStrategy[] calldata strategies, bool[] calldata whitelisted)
external;

function setExpressRelay(address _expressRelay) external;

function setPoolLens(address _poolLens) external;

function setHealthFactorThreshold(uint256 _healthFactorThreshold) external;
}
73 changes: 69 additions & 4 deletions contracts/IonicLiquidator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,23 @@ import "./external/uniswap/UniswapV2Library.sol";

import { ICErc20 } from "./compound/CTokenInterfaces.sol";

import "@pythnetwork/express-relay-sdk-solidity/IExpressRelay.sol";
import "@pythnetwork/express-relay-sdk-solidity/IExpressRelayFeeReceiver.sol";

import "./PoolLens.sol";

/**
* @title IonicLiquidator
* @author David Lucid <[email protected]> (https://github.com/davidlucid)
* @notice IonicLiquidator safely liquidates unhealthy borrowers (with flashloan support).
* @dev Do not transfer NATIVE or tokens directly to this address. Only send NATIVE here when using a method, and only approve tokens for transfer to here when using a method. Direct NATIVE transfers will be rejected and direct token transfers will be lost.
*/
contract IonicLiquidator is OwnableUpgradeable, ILiquidator, IUniswapV2Callee {
contract IonicLiquidator is OwnableUpgradeable, ILiquidator, IUniswapV2Callee, IExpressRelayFeeReceiver {
using AddressUpgradeable for address payable;
using SafeERC20Upgradeable for IERC20Upgradeable;

event VaultReceivedETH(address sender, uint256 amount, bytes permissionKey);

/**
* @dev W_NATIVE contract address.
*/
Expand Down Expand Up @@ -64,6 +71,27 @@ contract IonicLiquidator is OwnableUpgradeable, ILiquidator, IUniswapV2Callee {
*/
uint8 public flashSwapFee;

/**
* @dev Addres of Pyth Express Relay for preventing value leakage in liquidations.
*/
IExpressRelay public expressRelay;
/**
* @dev Pool Lens.
*/
PoolLens public lens;
/**
* @dev Health Factor below which PER permissioning is bypassed.
*/
uint256 public healthFactorThreshold;

modifier onlyPERPermissioned(address borrower, ICErc20 cToken) {
uint256 currentHealthFactor = lens.getHealthFactor(borrower, cToken.comptroller());
if (currentHealthFactor > healthFactorThreshold) {
require(expressRelay.isPermissioned(address(this), abi.encode(borrower)), "invalid liquidation");
}
_;
}

function initialize(
address _wtoken,
address _uniswapV2router,
Expand Down Expand Up @@ -119,15 +147,21 @@ contract IonicLiquidator is OwnableUpgradeable, ILiquidator, IUniswapV2Callee {
ICErc20 cErc20,
ICErc20 cTokenCollateral,
uint256 minOutputAmount
) external returns (uint256) {
) external onlyPERPermissioned(borrower, cTokenCollateral) returns (uint256) {
// Transfer tokens in, approve to cErc20, and liquidate borrow
require(repayAmount > 0, "Repay amount (transaction value) must be greater than 0.");
IERC20Upgradeable underlying = IERC20Upgradeable(cErc20.underlying());
underlying.safeTransferFrom(msg.sender, address(this), repayAmount);
justApprove(underlying, address(cErc20), repayAmount);
require(cErc20.liquidateBorrow(borrower, repayAmount, address(cTokenCollateral)) == 0, "Liquidation failed.");
// Transfer seized amount to sender
return transferSeizedFunds(address(cTokenCollateral), minOutputAmount);

// Redeem seized cTokens for underlying asset
uint256 seizedCTokenAmount = cTokenCollateral.balanceOf(address(this));
require(seizedCTokenAmount > 0, "No cTokens seized.");
uint256 redeemResult = cTokenCollateral.redeem(seizedCTokenAmount);
require(redeemResult == 0, "Error calling redeeming seized cToken: error code not equal to 0");

return transferSeizedFunds(address(cTokenCollateral.underlying()), minOutputAmount);
}

/**
Expand All @@ -150,6 +184,7 @@ contract IonicLiquidator is OwnableUpgradeable, ILiquidator, IUniswapV2Callee {
*/
function safeLiquidateToTokensWithFlashLoan(LiquidateToTokensWithFlashSwapVars calldata vars)
external
onlyPERPermissioned(vars.borrower, vars.cTokenCollateral)
returns (uint256)
{
// Input validation
Expand Down Expand Up @@ -199,6 +234,23 @@ contract IonicLiquidator is OwnableUpgradeable, ILiquidator, IUniswapV2Callee {
require(payable(msg.sender).isContract(), "Sender is not a contract.");
}

/**
* @notice receiveAuctionProceedings function - receives native token from the express relay
* You can use permission key to distribute the received funds to users who got liquidated, LPs, etc...
*/
function receiveAuctionProceedings(bytes calldata permissionKey) external payable {
emit VaultReceivedETH(msg.sender, msg.value, permissionKey);
}

function withdrawAll() external onlyOwner {
uint256 balance = address(this).balance;
require(balance > 0, "No Ether left to withdraw");

// Transfer all Ether to the owner
(bool sent, ) = msg.sender.call{ value: balance }("");
require(sent, "Failed to send Ether");
}

/**
* @dev Callback function for Uniswap flashloans.
*/
Expand Down Expand Up @@ -419,6 +471,19 @@ contract IonicLiquidator is OwnableUpgradeable, ILiquidator, IUniswapV2Callee {
}
}

function setExpressRelay(address _expressRelay) external onlyOwner {
expressRelay = IExpressRelay(_expressRelay);
}

function setPoolLens(address _poolLens) external onlyOwner {
lens = PoolLens(_poolLens);
}

function setHealthFactorThreshold(uint256 _healthFactorThreshold) external onlyOwner {
require(_healthFactorThreshold <= 1e18, "Invalid Health Factor Threshold");
healthFactorThreshold = _healthFactorThreshold;
}

/**
* @dev Redeem "special" collateral tokens (before swapping the output for borrowed tokens to be repaid via Uniswap).
* Public visibility because we have to call this function externally if called from a payable IonicLiquidator function (for some reason delegatecall fails when called with msg.value > 0).
Expand Down
71 changes: 67 additions & 4 deletions contracts/IonicUniV3Liquidator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,20 @@ import { IUniswapV3Quoter } from "./external/uniswap/quoter/interfaces/IUniswapV

import { ICErc20 } from "./compound/CTokenInterfaces.sol";

import "./PoolLens.sol";
import "@pythnetwork/express-relay-sdk-solidity/IExpressRelay.sol";
import "@pythnetwork/express-relay-sdk-solidity/IExpressRelayFeeReceiver.sol";

/**
* @title IonicUniV3Liquidator
* @author Veliko Minkov <[email protected]> (https://github.com/vminkov)
* @notice IonicUniV3Liquidator liquidates unhealthy borrowers with flashloan support.
*/
contract IonicUniV3Liquidator is OwnableUpgradeable, ILiquidator, IUniswapV3FlashCallback {
contract IonicUniV3Liquidator is OwnableUpgradeable, ILiquidator, IUniswapV3FlashCallback, IExpressRelayFeeReceiver {
using AddressUpgradeable for address payable;
using SafeERC20Upgradeable for IERC20Upgradeable;

event VaultReceivedETH(address sender, uint256 amount, bytes permissionKey);
/**
* @dev Cached liquidator profit exchange source.
* ERC20 token address or the zero address for NATIVE.
Expand All @@ -48,6 +53,27 @@ contract IonicUniV3Liquidator is OwnableUpgradeable, ILiquidator, IUniswapV3Flas
mapping(address => bool) public redemptionStrategiesWhitelist;
IUniswapV3Quoter public quoter;

/**
* @dev Addres of Pyth Express Relay for preventing value leakage in liquidations.
*/
IExpressRelay public expressRelay;
/**
* @dev Pool Lens.
*/
PoolLens public lens;
/**
* @dev Health Factor below which PER permissioning is bypassed.
*/
uint256 public healthFactorThreshold;

modifier onlyPERPermissioned(address borrower, ICErc20 cToken) {
uint256 currentHealthFactor = lens.getHealthFactor(borrower, cToken.comptroller());
if (currentHealthFactor > healthFactorThreshold) {
require(expressRelay.isPermissioned(address(this), abi.encode(borrower)), "invalid liquidation");
}
_;
}

function initialize(address _wtoken, address _quoter) external initializer {
__Ownable_init();
W_NATIVE_ADDRESS = _wtoken;
Expand All @@ -68,15 +94,21 @@ contract IonicUniV3Liquidator is OwnableUpgradeable, ILiquidator, IUniswapV3Flas
ICErc20 cErc20,
ICErc20 cTokenCollateral,
uint256 minOutputAmount
) external returns (uint256) {
) external onlyPERPermissioned(borrower, cTokenCollateral) returns (uint256) {
// Transfer tokens in, approve to cErc20, and liquidate borrow
require(repayAmount > 0, "Repay amount (transaction value) must be greater than 0.");
IERC20Upgradeable underlying = IERC20Upgradeable(cErc20.underlying());
underlying.safeTransferFrom(msg.sender, address(this), repayAmount);
underlying.approve(address(cErc20), repayAmount);
require(cErc20.liquidateBorrow(borrower, repayAmount, address(cTokenCollateral)) == 0, "Liquidation failed.");
// Transfer seized amount to sender
return transferSeizedFunds(address(cTokenCollateral), minOutputAmount);

// Redeem seized cTokens for underlying asset
uint256 seizedCTokenAmount = cTokenCollateral.balanceOf(address(this));
require(seizedCTokenAmount > 0, "No cTokens seized.");
uint256 redeemResult = cTokenCollateral.redeem(seizedCTokenAmount);
require(redeemResult == 0, "Error calling redeeming seized cToken: error code not equal to 0");

return transferSeizedFunds(address(cTokenCollateral.underlying()), minOutputAmount);
}

/**
Expand All @@ -95,6 +127,7 @@ contract IonicUniV3Liquidator is OwnableUpgradeable, ILiquidator, IUniswapV3Flas

function safeLiquidateToTokensWithFlashLoan(LiquidateToTokensWithFlashSwapVars calldata vars)
external
onlyPERPermissioned(vars.borrower, vars.cTokenCollateral)
returns (uint256)
{
// Input validation
Expand Down Expand Up @@ -144,6 +177,23 @@ contract IonicUniV3Liquidator is OwnableUpgradeable, ILiquidator, IUniswapV3Flas
require(payable(msg.sender).isContract(), "Sender is not a contract.");
}

/**
* @notice receiveAuctionProceedings function - receives native token from the express relay
* You can use permission key to distribute the received funds to users who got liquidated, LPs, etc...
*/
function receiveAuctionProceedings(bytes calldata permissionKey) external payable {
emit VaultReceivedETH(msg.sender, msg.value, permissionKey);
}

function withdrawAll() external onlyOwner {
uint256 balance = address(this).balance;
require(balance > 0, "No Ether left to withdraw");

// Transfer all Ether to the owner
(bool sent, ) = msg.sender.call{ value: balance }("");
require(sent, "Failed to send Ether");
}

/**
* @dev Callback function for Uniswap flashloans.
*/
Expand Down Expand Up @@ -341,6 +391,19 @@ contract IonicUniV3Liquidator is OwnableUpgradeable, ILiquidator, IUniswapV3Flas
}
}

function setExpressRelay(address _expressRelay) external onlyOwner {
expressRelay = IExpressRelay(_expressRelay);
}

function setPoolLens(address _poolLens) external onlyOwner {
lens = PoolLens(_poolLens);
}

function setHealthFactorThreshold(uint256 _healthFactorThreshold) external onlyOwner {
require(_healthFactorThreshold <= 1e18, "Invalid Health Factor Threshold");
healthFactorThreshold = _healthFactorThreshold;
}

/**
* @dev Redeem "special" collateral tokens (before swapping the output for borrowed tokens to be repaid via Uniswap).
* Public visibility because we have to call this function externally if called from a payable IonicLiquidator function (for some reason delegatecall fails when called with msg.value > 0).
Expand Down
23 changes: 18 additions & 5 deletions contracts/compound/Comptroller.sol
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,13 @@ contract Comptroller is ComptrollerBase, ComptrollerInterface, ComptrollerErrorR
require(borrowBalance >= repayAmount, "!borrow>repay");
} else {
/* The borrower must have shortfall in order to be liquidateable */
(Error err, , , uint256 shortfall) = getHypotheticalAccountLiquidityInternal(borrower, ICErc20(address(0)), 0, 0, 0);
(Error err, , , uint256 shortfall) = getHypotheticalAccountLiquidityInternal(
borrower,
ICErc20(address(0)),
0,
0,
0
);
if (err != Error.NO_ERROR) {
return uint256(err);
}
Expand Down Expand Up @@ -755,7 +761,7 @@ contract Comptroller is ComptrollerBase, ComptrollerInterface, ComptrollerErrorR
uint256 redeemTokens,
uint256 borrowAmount,
uint256 repayAmount
)
)
public
view
returns (
Expand All @@ -770,7 +776,13 @@ contract Comptroller is ComptrollerBase, ComptrollerInterface, ComptrollerErrorR
uint256 collateralValue,
uint256 liquidity,
uint256 shortfall
) = getHypotheticalAccountLiquidityInternal(account, ICErc20(cTokenModify), redeemTokens, borrowAmount, repayAmount);
) = getHypotheticalAccountLiquidityInternal(
account,
ICErc20(cTokenModify),
redeemTokens,
borrowAmount,
repayAmount
);
return (uint256(err), collateralValue, liquidity, shortfall);
}

Expand All @@ -791,7 +803,7 @@ contract Comptroller is ComptrollerBase, ComptrollerInterface, ComptrollerErrorR
uint256 redeemTokens,
uint256 borrowAmount,
uint256 repayAmount
)
)
internal
view
returns (
Expand Down Expand Up @@ -847,7 +859,8 @@ contract Comptroller is ComptrollerBase, ComptrollerInterface, ComptrollerErrorR

// accumulate the collateral value to sumCollateral
uint256 assetCollateralValue = mul_ScalarTruncate(vars.tokensToDenom, vars.cTokenBalance);
if (assetCollateralValue > vars.assetAsCollateralValueCap) assetCollateralValue = vars.assetAsCollateralValueCap;
if (assetCollateralValue > vars.assetAsCollateralValueCap)
assetCollateralValue = vars.assetAsCollateralValueCap;
vars.sumCollateral += assetCollateralValue;
}

Expand Down
Loading
Loading