Skip to content

Commit a252035

Browse files
authored
ExpressRelay:Moved EasyLend example (#18)
1 parent 8da293e commit a252035

File tree

13 files changed

+4402
-0
lines changed

13 files changed

+4402
-0
lines changed

express-relay/easy_lend/.eslintrc.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
root: true,
3+
parser: "@typescript-eslint/parser",
4+
plugins: ["@typescript-eslint"],
5+
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
6+
};

express-relay/easy_lend/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
lib/*
2+
out
3+
cache
4+
tslib
5+
!lib/README.md

express-relay/easy_lend/README.md

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

0 commit comments

Comments
 (0)