-
Notifications
You must be signed in to change notification settings - Fork 1
/
liquidation.py
230 lines (198 loc) · 6.61 KB
/
liquidation.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
"""
A script for liquidating underwater flash loans using
the Liquidator flash loaning contract.
"""
import os
from collections import namedtuple
import time
import random
import sys
from brownie import web3, convert, accounts
from brownie import Liquidator
import httpx
LIQUIDATOR_ADDRESS = "0x0" # Add your deployed liquidator address here.
WAVAX = "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7"
WETH = "0x49d5c2bdffac6ce2bfdb6640f4f80f226bc10bab"
WBTC = "0x50b7545627a5162f82a992c33b87adc75187b218"
USDC = "0xA7D7079b0FEaD91F3e65f86E8915Cb59c1a4C664"
USDT = "0xc7198437980c041c805a1edcba50c1ce5db95118"
DAI = "0xd586e7f844cea2f87f50152665bcbc2c279d8d70"
LINK = "0x5947bb275c521040051d82396192181b413227a3"
MIM = "0x130966628846bfd36ff31a822705796e8cb8c18d"
XJOE = "0x57319d41f71e81f3c65f2a47ca4e001ebafd4f33"
JAVAX = "0xC22F01ddc8010Ee05574028528614634684EC29e"
JWETH = "0x929f5caB61DFEc79a5431a7734a68D714C4633fa"
JWBTC = "0x3fE38b7b610C0ACD10296fEf69d9b18eB7a9eB1F"
JUSDC = "0xEd6AaF91a2B084bd594DBd1245be3691F9f637aC"
JUSDT = "0x8b650e26404AC6837539ca96812f0123601E4448"
JDAI = "0xc988c170d0E38197DC634A45bF00169C7Aa7CA19"
JLINK = "0x585E7bC75089eD111b656faA7aeb1104F5b96c15"
JMIM = "0xcE095A9657A02025081E0607c8D8b081c76A75ea"
JXJOE = "0xC146783a59807154F92084f9243eb139D58Da696"
# Provides a lookup for underlying token addresses.
JOE_TO_ERC20 = {
JAVAX: WAVAX,
JWETH: WETH,
JWBTC: WBTC,
JUSDC: USDC,
JUSDT: USDT,
JDAI: DAI,
JLINK: LINK,
JMIM: MIM,
JXJOE: XJOE,
}
TRADER_JOB_LENDING_SUBGRAPH_URL = (
"https://api.thegraph.com/subgraphs/name/traderjoe-xyz/lending")
UNDERWATER_ACCOUNTS_QUERY = """\
{{
accounts(where: {{
health_gt: {health_gt},
health_lt: {health_lt},
totalBorrowValueInUSD_gt: {borrow_value_usd_gt} }}
) {{
id
health
totalBorrowValueInUSD
totalCollateralValueInUSD
tokens {{
id
symbol
supplyBalanceUnderlying
borrowBalanceUnderlying
enteredMarket
}}
}}
}}
"""
MARKET_QUERY = """\
{
markets {
id
symbol
underlyingPriceUSD
}
}
"""
LiquidationParameters = namedtuple(
'LiquidationParameters', [
'borrower',
'liquidation_contract',
'liquidation_underlying',
'collateral_contract',
'collateral_underlying',
'flashloan_contract',
'flashloan_underlying',
])
def query_underwater_accounts(
health_lt=1.0,
health_gt=0,
borrow_value_usd_gt=0
):
"""
Query thegraph API to find loans with given health values and underlying
borrowed collaterall.
"""
query = UNDERWATER_ACCOUNTS_QUERY.format(
health_lt=health_lt,
health_gt=health_gt,
borrow_value_usd_gt=borrow_value_usd_gt,
)
response = httpx.post(
TRADER_JOB_LENDING_SUBGRAPH_URL,
json={"query": query},
)
response.raise_for_status()
return response.json()['data']['accounts']
def query_underling_price_usd():
"""
Get the current USD price for all banker joe markets
"""
response = httpx.post(
TRADER_JOB_LENDING_SUBGRAPH_URL,
json={"query": MARKET_QUERY},
)
response.raise_for_status()
return {
oracle['symbol']: oracle['underlyingPriceUSD']
for oracle in
response.json()['data']['markets']
}
def liquidation_parameters(accounts):
"""
Iterator over a series of underwater accounts.
Yields `LiquidationParameters` named tuples containing
the parameters for our liquidation contract.
"""
# supplyBalanceUnderlying > 0
# enterMarket == true (otherwise it’s not posted as collateral)
# Must have enough supplyBalanceUnderlying to seize 50% of borrow value
for account in accounts:
# We use a naieve algorithm here. We first find the max
# seizable collateral and then find an account that has
# borrowed more than double of this. Our contract doesn't
# do this logic for us so we either get all or nothing here.
# If there is no single collateral we can seize to repay
# a full amount the account is skipped (lucky borrower).
max_seizable = max([
token
for token in account['tokens']
if token['enteredMarket'] is True
and float(token['supplyBalanceUnderlying']) > 0],
key=lambda t: float(t['supplyBalanceUnderlying']))
max_repayable = max([
token
for token in account['tokens']
if token['enteredMarket'] is True
and (
(float(token['borrowBalanceUnderlying']) / 2)
< float(max_seizable['supplyBalanceUnderlying']))],
key=lambda t: float(t['borrowBalanceUnderlying']))
if not max_seizable and max_repayable:
continue
# Addresses aren't checksumed in graph response.
repayable = convert.to_address(max_repayable['id'].split('-')[0])
seizable = convert.to_address(max_seizable['id'].split('-')[0])
# For flash loaning we just choose one token not represented here.
flash_loanable = set(JOE_TO_ERC20)
flash_loanable.remove(repayable)
try:
flash_loanable.remove(seizable)
except KeyError:
# collateral and borrowed same token.
pass
# Choose a random token for our flash loan.
flash_loan = random.choice(list(flash_loanable))
yield LiquidationParameters(
convert.to_address(account['id']),
repayable,
JOE_TO_ERC20[repayable],
seizable,
JOE_TO_ERC20[seizable],
flash_loan,
JOE_TO_ERC20[flash_loan],
)
def main():
"""
Our main function that.
1. Listens for new blocks.
2. Queries the graph.
3. Sends liquidation params to our flash loan contract.
"""
# Our pre-deployed liquidator contract.
liquidator = Liquidator.at(LIQUIDATOR_ADDRESS)
# Filter for new blocks.
new_blocks = web3.eth.filter('latest')
# Continuous loop waiting for new blocks.
while True:
if new_blocks.get_new_entries():
accounts = query_underwater_accounts()
for liquidation_params in liquidation_parameters(accounts):
try:
Liquidator.liquidateLoan(*liquidation_params, {'from': accounts[0]})
except brownie.exceptions.VirtualMachineError as exc:
print(f"Exception liquidation loan {exc}", file=sys.stderr)
else:
# Call to discord etc.
print(f"Liquidated loan {liquidation_params}")
if __name__ == "__main__":
main()