EasyLend.sol 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. // SPDX-License-Identifier: Apache 2
  2. pragma solidity ^0.8.13;
  3. import "./EasyLendStructs.sol";
  4. import "./EasyLendErrors.sol";
  5. import "forge-std/StdMath.sol";
  6. import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
  7. import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
  8. import "@openzeppelin/contracts/utils/Strings.sol";
  9. import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
  10. import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
  11. import "@pythnetwork/express-relay-sdk-solidity/IExpressRelayFeeReceiver.sol";
  12. import "@pythnetwork/express-relay-sdk-solidity/IExpressRelay.sol";
  13. contract EasyLend is IExpressRelayFeeReceiver {
  14. using SafeERC20 for IERC20;
  15. event VaultReceivedETH(address sender, uint256 amount, bytes permissionKey);
  16. uint256 _nVaults;
  17. address public immutable expressRelay;
  18. mapping(uint256 => Vault) _vaults;
  19. address _oracle;
  20. bool _allowUndercollateralized;
  21. /**
  22. * @notice EasyLend constructor - Initializes a new token vault contract with given parameters
  23. *
  24. * @param expressRelayAddress: address of the express relay
  25. * @param oracleAddress: address of the oracle contract
  26. * @param allowUndercollateralized: boolean to allow undercollateralized vaults to be created and updated. Can be set to true for testing.
  27. */
  28. constructor(
  29. address expressRelayAddress,
  30. address oracleAddress,
  31. bool allowUndercollateralized
  32. ) {
  33. _nVaults = 0;
  34. expressRelay = expressRelayAddress;
  35. _oracle = oracleAddress;
  36. _allowUndercollateralized = allowUndercollateralized;
  37. }
  38. /**
  39. * @notice getLastVaultId function - getter function to get the id of the next vault to be created
  40. * Ids are sequential and start from 0
  41. */
  42. function getLastVaultId() public view returns (uint256) {
  43. return _nVaults;
  44. }
  45. /**
  46. * @notice convertToUint function - converts a Pyth price struct to a uint256 representing the price of an asset
  47. *
  48. * @param price: Pyth price struct to be converted
  49. * @param targetDecimals: target number of decimals for the output
  50. */
  51. function convertToUint(
  52. PythStructs.Price memory price,
  53. uint8 targetDecimals
  54. ) private pure returns (uint256) {
  55. if (price.price < 0 || price.expo > 0 || price.expo < -255) {
  56. revert InvalidPriceExponent();
  57. }
  58. uint8 priceDecimals = uint8(uint32(-1 * price.expo));
  59. if (targetDecimals >= priceDecimals) {
  60. return
  61. uint(uint64(price.price)) *
  62. 10 ** uint32(targetDecimals - priceDecimals);
  63. } else {
  64. return
  65. uint(uint64(price.price)) /
  66. 10 ** uint32(priceDecimals - targetDecimals);
  67. }
  68. }
  69. /**
  70. * @notice getPrice function - retrieves price of a given token from the oracle
  71. *
  72. * @param id: price feed Id of the token
  73. */
  74. function _getPrice(bytes32 id) internal view returns (uint256) {
  75. IPyth oracle = IPyth(payable(_oracle));
  76. return convertToUint(oracle.getPriceNoOlderThan(id, 60), 18);
  77. }
  78. function getAllowUndercollateralized() public view returns (bool) {
  79. return _allowUndercollateralized;
  80. }
  81. function getOracle() public view returns (address) {
  82. return _oracle;
  83. }
  84. /**
  85. * @notice getVaultHealth function - calculates vault collateral/debt ratio
  86. *
  87. * @param vaultId: Id of the vault for which to calculate health
  88. */
  89. function getVaultHealth(uint256 vaultId) public view returns (uint256) {
  90. Vault memory vault = _vaults[vaultId];
  91. return _getVaultHealth(vault);
  92. }
  93. /**
  94. * @notice _getVaultHealth function - calculates vault collateral/debt ratio using the on-chain price feeds.
  95. * In a real world scenario, caller should ensure that the price feeds are up to date before calling this function.
  96. *
  97. * @param vault: vault struct containing vault parameters
  98. */
  99. function _getVaultHealth(
  100. Vault memory vault
  101. ) internal view returns (uint256) {
  102. uint256 priceCollateral = _getPrice(vault.tokenPriceFeedIdCollateral);
  103. uint256 priceDebt = _getPrice(vault.tokenPriceFeedIdDebt);
  104. if (priceCollateral < 0) {
  105. revert NegativePrice();
  106. }
  107. if (priceDebt < 0) {
  108. revert NegativePrice();
  109. }
  110. uint256 valueCollateral = priceCollateral * vault.amountCollateral;
  111. uint256 valueDebt = priceDebt * vault.amountDebt;
  112. return (valueCollateral * 1_000_000_000_000_000_000) / valueDebt;
  113. }
  114. /**
  115. * @notice createVault function - creates a vault
  116. *
  117. * @param tokenCollateral: address of the collateral token of the vault
  118. * @param tokenDebt: address of the debt token of the vault
  119. * @param amountCollateral: amount of collateral tokens in the vault
  120. * @param amountDebt: amount of debt tokens in the vault
  121. * @param minHealthRatio: minimum health ratio of the vault, 10**18 is 100%
  122. * @param minPermissionlessHealthRatio: minimum health ratio of the vault before permissionless liquidations are allowed. This should be less than minHealthRatio
  123. * @param tokenPriceFeedIdCollateral: price feed Id of the collateral token
  124. * @param tokenPriceFeedIdDebt: price feed Id of the debt token
  125. * @param updateData: data to update price feeds with
  126. */
  127. function createVault(
  128. address tokenCollateral,
  129. address tokenDebt,
  130. uint256 amountCollateral,
  131. uint256 amountDebt,
  132. uint256 minHealthRatio,
  133. uint256 minPermissionlessHealthRatio,
  134. bytes32 tokenPriceFeedIdCollateral,
  135. bytes32 tokenPriceFeedIdDebt,
  136. bytes[] calldata updateData
  137. ) public payable returns (uint256) {
  138. _updatePriceFeeds(updateData);
  139. Vault memory vault = Vault(
  140. tokenCollateral,
  141. tokenDebt,
  142. amountCollateral,
  143. amountDebt,
  144. minHealthRatio,
  145. minPermissionlessHealthRatio,
  146. tokenPriceFeedIdCollateral,
  147. tokenPriceFeedIdDebt
  148. );
  149. if (minPermissionlessHealthRatio > minHealthRatio) {
  150. revert InvalidHealthRatios();
  151. }
  152. if (
  153. !_allowUndercollateralized &&
  154. _getVaultHealth(vault) < vault.minHealthRatio
  155. ) {
  156. revert UncollateralizedVaultCreation();
  157. }
  158. IERC20(vault.tokenCollateral).safeTransferFrom(
  159. msg.sender,
  160. address(this),
  161. vault.amountCollateral
  162. );
  163. IERC20(vault.tokenDebt).safeTransfer(msg.sender, vault.amountDebt);
  164. _vaults[_nVaults] = vault;
  165. _nVaults += 1;
  166. return _nVaults;
  167. }
  168. /**
  169. * @notice updateVault function - updates a vault's collateral and debt amounts
  170. *
  171. * @param vaultId: Id of the vault to be updated
  172. * @param deltaCollateral: delta change to collateral amount (+ means adding collateral tokens, - means removing collateral tokens)
  173. * @param deltaDebt: delta change to debt amount (+ means withdrawing debt tokens from protocol, - means resending debt tokens to protocol)
  174. */
  175. function updateVault(
  176. uint256 vaultId,
  177. int256 deltaCollateral,
  178. int256 deltaDebt
  179. ) public {
  180. Vault memory vault = _vaults[vaultId];
  181. uint256 qCollateral = stdMath.abs(deltaCollateral);
  182. uint256 qDebt = stdMath.abs(deltaDebt);
  183. bool withdrawExcessiveCollateral = (deltaCollateral < 0) &&
  184. (qCollateral > vault.amountCollateral);
  185. if (withdrawExcessiveCollateral) {
  186. revert InvalidVaultUpdate();
  187. }
  188. uint256 futureCollateral = (deltaCollateral >= 0)
  189. ? (vault.amountCollateral + qCollateral)
  190. : (vault.amountCollateral - qCollateral);
  191. uint256 futureDebt = (deltaDebt >= 0)
  192. ? (vault.amountDebt + qDebt)
  193. : (vault.amountDebt - qDebt);
  194. vault.amountCollateral = futureCollateral;
  195. vault.amountDebt = futureDebt;
  196. if (
  197. !_allowUndercollateralized &&
  198. _getVaultHealth(vault) < vault.minHealthRatio
  199. ) {
  200. revert InvalidVaultUpdate();
  201. }
  202. // update collateral position
  203. if (deltaCollateral >= 0) {
  204. // sender adds more collateral to their vault
  205. IERC20(vault.tokenCollateral).safeTransferFrom(
  206. msg.sender,
  207. address(this),
  208. qCollateral
  209. );
  210. _vaults[vaultId].amountCollateral += qCollateral;
  211. } else {
  212. // sender takes back collateral from their vault
  213. IERC20(vault.tokenCollateral).safeTransfer(msg.sender, qCollateral);
  214. _vaults[vaultId].amountCollateral -= qCollateral;
  215. }
  216. // update debt position
  217. if (deltaDebt >= 0) {
  218. // sender takes out more debt position
  219. IERC20(vault.tokenDebt).safeTransfer(msg.sender, qDebt);
  220. _vaults[vaultId].amountDebt += qDebt;
  221. } else {
  222. // sender sends back debt tokens
  223. IERC20(vault.tokenDebt).safeTransferFrom(
  224. msg.sender,
  225. address(this),
  226. qDebt
  227. );
  228. _vaults[vaultId].amountDebt -= qDebt;
  229. }
  230. }
  231. /**
  232. * @notice getVault function - getter function to get a vault's parameters
  233. *
  234. * @param vaultId: Id of the vault
  235. */
  236. function getVault(uint256 vaultId) public view returns (Vault memory) {
  237. return _vaults[vaultId];
  238. }
  239. /**
  240. * @notice _updatePriceFeeds function - updates the specified price feeds with given data
  241. *
  242. * @param updateData: data to update price feeds with
  243. */
  244. function _updatePriceFeeds(bytes[] calldata updateData) internal {
  245. if (updateData.length == 0) {
  246. return;
  247. }
  248. IPyth oracle = IPyth(payable(_oracle));
  249. oracle.updatePriceFeeds{value: msg.value}(updateData);
  250. }
  251. /**
  252. * @notice liquidate function - liquidates a vault
  253. * This function calculates the health of the vault and based on the vault parameters one of the following actions is taken:
  254. * 1. If health >= minHealthRatio, don't liquidate
  255. * 2. If minHealthRatio > health >= minPermissionlessHealthRatio, only liquidate if the vault is permissioned via express relay
  256. * 3. If minPermissionlessHealthRatio > health, liquidate no matter what
  257. *
  258. * @param vaultId: Id of the vault to be liquidated
  259. */
  260. function liquidate(uint256 vaultId) public {
  261. Vault memory vault = _vaults[vaultId];
  262. uint256 vaultHealth = _getVaultHealth(vault);
  263. // if vault health is above the minimum health ratio, don't liquidate
  264. if (vaultHealth >= vault.minHealthRatio) {
  265. revert InvalidLiquidation();
  266. }
  267. if (vaultHealth >= vault.minPermissionlessHealthRatio) {
  268. // if vault health is below the minimum health ratio but above the minimum permissionless health ratio,
  269. // only liquidate if permissioned
  270. if (
  271. !IExpressRelay(expressRelay).isPermissioned(
  272. address(this), // protocol fee receiver
  273. abi.encode(vaultId) // vault id uniquely represents the opportunity and can be used as permission id
  274. )
  275. ) {
  276. revert InvalidLiquidation();
  277. }
  278. }
  279. IERC20(vault.tokenDebt).transferFrom(
  280. msg.sender,
  281. address(this),
  282. vault.amountDebt
  283. );
  284. IERC20(vault.tokenCollateral).transfer(
  285. msg.sender,
  286. vault.amountCollateral
  287. );
  288. _vaults[vaultId].amountCollateral = 0;
  289. _vaults[vaultId].amountDebt = 0;
  290. }
  291. /**
  292. * @notice liquidateWithPriceUpdate function - liquidates a vault after updating the specified price feeds with given data
  293. *
  294. * @param vaultId: Id of the vault to be liquidated
  295. * @param updateData: data to update price feeds with
  296. */
  297. function liquidateWithPriceUpdate(
  298. uint256 vaultId,
  299. bytes[] calldata updateData
  300. ) external payable {
  301. _updatePriceFeeds(updateData);
  302. liquidate(vaultId);
  303. }
  304. /**
  305. * @notice receiveAuctionProceedings function - receives native token from the express relay
  306. * You can use permission key to distribute the received funds to users who got liquidated, LPs, etc...
  307. *
  308. * @param permissionKey: permission key that was used for the auction
  309. */
  310. function receiveAuctionProceedings(
  311. bytes calldata permissionKey
  312. ) external payable {
  313. emit VaultReceivedETH(msg.sender, msg.value, permissionKey);
  314. }
  315. receive() external payable {}
  316. }