// Copyright (C) 2024 Lavra Holdings Limited - All Rights Reserved pragma solidity ^0.8.13; import "./EasyLendStructs.sol"; import "./EasyLendErrors.sol"; import "forge-std/StdMath.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol"; import "@pythnetwork/pyth-sdk-solidity/IPyth.sol"; import "@pythnetwork/express-relay-sdk-solidity/IExpressRelayFeeReceiver.sol"; import "@pythnetwork/express-relay-sdk-solidity/IExpressRelay.sol"; contract EasyLend is IExpressRelayFeeReceiver { using SafeERC20 for IERC20; event VaultReceivedETH(address sender, uint256 amount, bytes permissionKey); uint256 _nVaults; address public immutable expressRelay; mapping(uint256 => Vault) _vaults; address _oracle; bool _allowUndercollateralized; /** * @notice EasyLend constructor - Initializes a new token vault contract with given parameters * * @param expressRelayAddress: address of the express relay * @param oracleAddress: address of the oracle contract * @param allowUndercollateralized: boolean to allow undercollateralized vaults to be created and updated. Can be set to true for testing. */ constructor( address expressRelayAddress, address oracleAddress, bool allowUndercollateralized ) { _nVaults = 0; expressRelay = expressRelayAddress; _oracle = oracleAddress; _allowUndercollateralized = allowUndercollateralized; } /** * @notice getLastVaultId function - getter function to get the id of the next vault to be created * Ids are sequential and start from 0 */ function getLastVaultId() public view returns (uint256) { return _nVaults; } /** * @notice convertToUint function - converts a Pyth price struct to a uint256 representing the price of an asset * * @param price: Pyth price struct to be converted * @param targetDecimals: target number of decimals for the output */ function convertToUint( PythStructs.Price memory price, uint8 targetDecimals ) private pure returns (uint256) { if (price.price < 0 || price.expo > 0 || price.expo < -255) { revert InvalidPriceExponent(); } uint8 priceDecimals = uint8(uint32(-1 * price.expo)); if (targetDecimals >= priceDecimals) { return uint(uint64(price.price)) * 10 ** uint32(targetDecimals - priceDecimals); } else { return uint(uint64(price.price)) / 10 ** uint32(priceDecimals - targetDecimals); } } /** * @notice getPrice function - retrieves price of a given token from the oracle * * @param id: price feed Id of the token */ function _getPrice(bytes32 id) internal view returns (uint256) { IPyth oracle = IPyth(payable(_oracle)); return convertToUint(oracle.getPrice(id), 18); } function getAllowUndercollateralized() public view returns (bool) { return _allowUndercollateralized; } function getOracle() public view returns (address) { return _oracle; } /** * @notice getVaultHealth function - calculates vault collateral/debt ratio * * @param vaultId: Id of the vault for which to calculate health */ function getVaultHealth(uint256 vaultId) public view returns (uint256) { Vault memory vault = _vaults[vaultId]; return _getVaultHealth(vault); } /** * @notice _getVaultHealth function - calculates vault collateral/debt ratio using the on-chain price feeds. * In a real world scenario, caller should ensure that the price feeds are up to date before calling this function. * * @param vault: vault struct containing vault parameters */ function _getVaultHealth( Vault memory vault ) internal view returns (uint256) { uint256 priceCollateral = _getPrice(vault.tokenIdCollateral); uint256 priceDebt = _getPrice(vault.tokenIdDebt); if (priceCollateral < 0) { revert NegativePrice(); } if (priceDebt < 0) { revert NegativePrice(); } uint256 valueCollateral = priceCollateral * vault.amountCollateral; uint256 valueDebt = priceDebt * vault.amountDebt; return (valueCollateral * 1_000_000_000_000_000_000) / valueDebt; } /** * @notice createVault function - creates a vault * * @param tokenCollateral: address of the collateral token of the vault * @param tokenDebt: address of the debt token of the vault * @param amountCollateral: amount of collateral tokens in the vault * @param amountDebt: amount of debt tokens in the vault * @param minHealthRatio: minimum health ratio of the vault, 10**18 is 100% * @param minPermissionlessHealthRatio: minimum health ratio of the vault before permissionless liquidations are allowed. This should be less than minHealthRatio * @param tokenIdCollateral: price feed Id of the collateral token * @param tokenIdDebt: price feed Id of the debt token * @param updateData: data to update price feeds with */ function createVault( address tokenCollateral, address tokenDebt, uint256 amountCollateral, uint256 amountDebt, uint256 minHealthRatio, uint256 minPermissionlessHealthRatio, bytes32 tokenIdCollateral, bytes32 tokenIdDebt, bytes[] calldata updateData ) public payable returns (uint256) { _updatePriceFeeds(updateData); Vault memory vault = Vault( tokenCollateral, tokenDebt, amountCollateral, amountDebt, minHealthRatio, minPermissionlessHealthRatio, tokenIdCollateral, tokenIdDebt ); if (minPermissionlessHealthRatio > minHealthRatio) { revert InvalidHealthRatios(); } if ( !_allowUndercollateralized && _getVaultHealth(vault) < vault.minHealthRatio ) { revert UncollateralizedVaultCreation(); } IERC20(vault.tokenCollateral).safeTransferFrom( msg.sender, address(this), vault.amountCollateral ); IERC20(vault.tokenDebt).safeTransfer(msg.sender, vault.amountDebt); _vaults[_nVaults] = vault; _nVaults += 1; return _nVaults; } /** * @notice updateVault function - updates a vault's collateral and debt amounts * * @param vaultId: Id of the vault to be updated * @param deltaCollateral: delta change to collateral amount (+ means adding collateral tokens, - means removing collateral tokens) * @param deltaDebt: delta change to debt amount (+ means withdrawing debt tokens from protocol, - means resending debt tokens to protocol) */ function updateVault( uint256 vaultId, int256 deltaCollateral, int256 deltaDebt ) public { Vault memory vault = _vaults[vaultId]; uint256 qCollateral = stdMath.abs(deltaCollateral); uint256 qDebt = stdMath.abs(deltaDebt); bool withdrawExcessiveCollateral = (deltaCollateral < 0) && (qCollateral > vault.amountCollateral); if (withdrawExcessiveCollateral) { revert InvalidVaultUpdate(); } uint256 futureCollateral = (deltaCollateral >= 0) ? (vault.amountCollateral + qCollateral) : (vault.amountCollateral - qCollateral); uint256 futureDebt = (deltaDebt >= 0) ? (vault.amountDebt + qDebt) : (vault.amountDebt - qDebt); vault.amountCollateral = futureCollateral; vault.amountDebt = futureDebt; if ( !_allowUndercollateralized && _getVaultHealth(vault) < vault.minHealthRatio ) { revert InvalidVaultUpdate(); } // update collateral position if (deltaCollateral >= 0) { // sender adds more collateral to their vault IERC20(vault.tokenCollateral).safeTransferFrom( msg.sender, address(this), qCollateral ); _vaults[vaultId].amountCollateral += qCollateral; } else { // sender takes back collateral from their vault IERC20(vault.tokenCollateral).safeTransfer(msg.sender, qCollateral); _vaults[vaultId].amountCollateral -= qCollateral; } // update debt position if (deltaDebt >= 0) { // sender takes out more debt position IERC20(vault.tokenDebt).safeTransfer(msg.sender, qDebt); _vaults[vaultId].amountDebt += qDebt; } else { // sender sends back debt tokens IERC20(vault.tokenDebt).safeTransferFrom( msg.sender, address(this), qDebt ); _vaults[vaultId].amountDebt -= qDebt; } } /** * @notice getVault function - getter function to get a vault's parameters * * @param vaultId: Id of the vault */ function getVault(uint256 vaultId) public view returns (Vault memory) { return _vaults[vaultId]; } /** * @notice _updatePriceFeeds function - updates the specified price feeds with given data * * @param updateData: data to update price feeds with */ function _updatePriceFeeds(bytes[] calldata updateData) internal { if (updateData.length == 0) { return; } IPyth oracle = IPyth(payable(_oracle)); oracle.updatePriceFeeds{value: msg.value}(updateData); } /** * @notice liquidate function - liquidates a vault * This function calculates the health of the vault and based on the vault parameters one of the following actions is taken: * 1. If health >= minHealthRatio, don't liquidate * 2. If minHealthRatio > health >= minPermissionlessHealthRatio, only liquidate if the vault is permissioned via express relay * 3. If minPermissionlessHealthRatio > health, liquidate no matter what * * @param vaultId: Id of the vault to be liquidated */ function liquidate(uint256 vaultId) public { Vault memory vault = _vaults[vaultId]; uint256 vaultHealth = _getVaultHealth(vault); // if vault health is above the minimum health ratio, don't liquidate if (vaultHealth >= vault.minHealthRatio) { revert InvalidLiquidation(); } if (vaultHealth >= vault.minPermissionlessHealthRatio) { // if vault health is below the minimum health ratio but above the minimum permissionless health ratio, // only liquidate if permissioned if ( !IExpressRelay(expressRelay).isPermissioned( address(this), // protocol fee receiver abi.encode(vaultId) // vault id uniquely represents the opportunity and can be used as permission id ) ) { revert InvalidLiquidation(); } } IERC20(vault.tokenDebt).transferFrom( msg.sender, address(this), vault.amountDebt ); IERC20(vault.tokenCollateral).transfer( msg.sender, vault.amountCollateral ); _vaults[vaultId].amountCollateral = 0; _vaults[vaultId].amountDebt = 0; } /** * @notice liquidateWithPriceUpdate function - liquidates a vault after updating the specified price feeds with given data * * @param vaultId: Id of the vault to be liquidated * @param updateData: data to update price feeds with */ function liquidateWithPriceUpdate( uint256 vaultId, bytes[] calldata updateData ) external payable { _updatePriceFeeds(updateData); liquidate(vaultId); } /** * @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... * * @param permissionKey: permission key that was used for the auction */ function receiveAuctionProceedings( bytes calldata permissionKey ) external payable { emit VaultReceivedETH(msg.sender, msg.value, permissionKey); } receive() external payable {} }