Skip to content

Commit b62241c

Browse files
feat: Fetch all deployments from registry
1 parent 1d736e6 commit b62241c

File tree

4 files changed

+278
-95
lines changed

4 files changed

+278
-95
lines changed

bots/README.md

Lines changed: 91 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,21 @@ Running the Bot:
2626

2727
```bash
2828
liquidator-service \
29-
--markets market1.testnet \
29+
--registries registry1.testnet --registries registry2.testnet \
3030
--signer-key ed25519:\<YOUR_PRIVATE_KEY_HERE> \
3131
--signer-account liquidator.testnet \
3232
--asset usdc.testnet \
3333
--swap rhea-swap \
3434
--network testnet \
3535
--timeout 60 \
3636
--concurrency 10 \
37-
--interval 600
37+
--interval 600 \
38+
--registry-refresh-interval 3600
3839
```
3940

4041
Arguments:
4142

42-
- `--markets`: A list of markets to monitor for liquidations (e.g., templar-market1.testnet).
43+
- `--registries`: A list of registries to query markets from which will be monitored for liquidations (e.g., templar-registry1.testnet).
4344
- `--signer-key`: The private key of the signer account used to sign transactions.
4445
- `--signer-account`: The NEAR account that will perform the liquidations (e.g., templar-liquidator.testnet).
4546
- `--asset`: The asset to liquidate NEP-141 token account used for repayments (e.g., usdc.testnet).
@@ -48,10 +49,12 @@ Arguments:
4849
- `--timeout`: The timeout for RPC calls in seconds (default is 60 seconds).
4950
- `--concurrency`: The number of concurrent liquidation attempts (default is 10).
5051
- `--interval`: The interval in seconds for the service to check for liquidatable positions (default is 600 seconds).
52+
- `--registry-refresh-interval`: The interval in seconds for the service to check for new markets on the registries (default is 3600 seconds - 1 hour).
5153

5254
How it works:
5355

54-
1. The bot initializes a Liquidator object for each market specified in the `--markets` argument.
56+
1. The bot fetches all deployments for each registry specified in the `--registryes` argument.
57+
1. The bot initializes a Liquidator object for each market fetched.
5558
1. It continuously checks the status of borrowers in each market.
5659
1. If a borrower is found to be liquidatable, it calculates the liquidation amount based on the borrower's collateral and debt.
5760
1. It sends an `ft_transfer_call` RPC call to the smart contract to trigger the liquidation process.
@@ -66,7 +69,7 @@ Liquidation Logic:
6669
The liquidation logic is encapsulated within the `Liquidator` object, which is responsible for:
6770

6871
- Checking a borrower's status to determine if they are below the required collateralization ratio.
69-
- Calculating the liquidation amount based on the borrower's collateral and debt. (This calculation should be implemented by the liquidator according to their specific strategy or requirements.)
72+
- Calculating the liquidation amount based on the borrower's collateral and debt.
7073

7174
```rust
7275
#[instrument(skip(self), level = "debug")]
@@ -76,17 +79,7 @@ async fn liquidation_amount(
7679
oracle_response: &OracleResponse,
7780
configuration: MarketConfiguration,
7881
) -> LiquidatorResult<(U128, U128)> {
79-
// TODO: Calculate optimal liquidation amount
80-
// For purposes of this example implementation we will just use the minimum acceptable
81-
// liquidation amount.
82-
// Costs to take into account here are:
83-
// - Gas fees
84-
// - Price impact
85-
// - Slippage
86-
// All of this would be used in calculating both the optimal liquidation amount and wether to
87-
// perform full or partial liquidation
8882
let borrow_asset = &configuration.borrow_asset;
89-
let collateral_asset = &configuration.collateral_asset;
9083
let price_pair = configuration
9184
.price_oracle_configuration
9285
.create_price_pair(oracle_response)?;
@@ -111,20 +104,32 @@ async fn liquidation_amount(
111104
)
112105
.await
113106
.map_err(LiquidatorError::QuoteError)?;
114-
let _quote_after_liquidate = self
115-
.swap
116-
.quote(
117-
// TODO: Enable multitoken swaps
118-
&collateral_asset.contract_id(),
119-
&self.asset,
120-
position.collateral_asset_deposit.into(),
121-
)
122-
.await
123-
.map_err(LiquidatorError::QuoteError)?;
124107
Ok((quote_to_liquidate, min_liquidation_amount.into()))
125108
}
126109
```
127110

111+
- Deciding on whether the liquidation should happen or not (This calculation should be implemented by the liquidator according to their specific strategy or requirements.)
112+
113+
```rust
114+
#[instrument(skip(self), level = "debug")]
115+
pub async fn should_liquidate(
116+
&self,
117+
swap_amount: U128,
118+
liquidation_amount: U128,
119+
) -> LiquidatorResult<bool> {
120+
// TODO: Calculate optimal liquidation amount
121+
// For purposes of this example implementation we will just use the minimum acceptable
122+
// liquidation amount.
123+
// Costs to take into account here are:
124+
// - Gas fees
125+
// - Price impact
126+
// - Slippage
127+
// All of this would be used in calculating both the optimal liquidation amount and wether to
128+
// perform full or partial liquidation
129+
Ok(true)
130+
}
131+
```
132+
128133
- Sending the `ft_transfer_call` RPC call to the borrow asset contract to trigger liquidation.
129134
- Handling errors and retries for failed liquidation attempts.
130135
- Logging the results of each liquidation attempt for monitoring and debugging purposes.
@@ -172,6 +177,67 @@ async fn get_oracle_prices(
172177

173178
The liquidator will fetch the price data from the oracle contract in order to execute the liquidation and gauge whether the liquidation is profitable.
174179

180+
### Fetching deployed markets
181+
182+
```rust
183+
#[instrument(skip(client), level = "debug")]
184+
pub async fn list_deployments(
185+
client: &JsonRpcClient,
186+
registry: AccountId,
187+
count: Option<u32>,
188+
offset: Option<u32>,
189+
) -> RpcResult<Vec<AccountId>> {
190+
let mut all_deployments = Vec::new();
191+
let page_size = 500;
192+
let mut current_offset = 0;
193+
194+
loop {
195+
let params = json!({
196+
"offset": current_offset,
197+
"count": page_size,
198+
});
199+
200+
let page =
201+
view::<Vec<AccountId>>(client, registry.clone(), "list_deployments", params).await?;
202+
203+
let fetched = page.len();
204+
205+
if fetched == 0 {
206+
break;
207+
}
208+
209+
all_deployments.extend(page);
210+
current_offset += fetched;
211+
212+
if fetched < page_size {
213+
break;
214+
}
215+
}
216+
217+
Ok(all_deployments)
218+
}
219+
220+
#[instrument(skip(client), level = "debug")]
221+
pub async fn list_all_deployments(
222+
client: JsonRpcClient,
223+
registries: Vec<AccountId>,
224+
concurrency: usize,
225+
) -> RpcResult<Vec<AccountId>> {
226+
let all_markets: Vec<AccountId> = futures::stream::iter(registries)
227+
.map(|registry| {
228+
let client = client.clone();
229+
async move { list_deployments(&client, registry, None, None).await }
230+
})
231+
.buffer_unordered(concurrency)
232+
.try_concat()
233+
.await?;
234+
235+
Ok(all_markets)
236+
}
237+
```
238+
239+
The liquidator will periodically fetch all registries for all of their deployments (markets).
240+
175241
### Getting the borrow positions for a market
176242

177243
```rust

bots/src/bin/liquidator-bot.rs

Lines changed: 114 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,78 @@
1-
use std::time::Duration;
1+
use std::{
2+
collections::HashMap,
3+
sync::Arc,
4+
time::{Duration, Instant},
5+
};
26

37
use clap::Parser;
4-
use templar_bots::liquidator::{setup_liquidators, Args, LiquidatorResult};
8+
use futures::{StreamExt, TryStreamExt};
9+
use near_crypto::InMemorySigner;
10+
use near_jsonrpc_client::JsonRpcClient;
11+
use near_sdk::{serde_json::json, AccountId};
12+
use templar_bots::{
13+
liquidator::{Args, Liquidator, LiquidatorError, LiquidatorResult},
14+
near::{view, RpcResult},
15+
swap::{RheaSwap, SwapType},
16+
};
517
use tokio::time::sleep;
6-
use tracing::info;
18+
use tracing::{info, instrument};
719
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
820

21+
#[instrument(skip(client), level = "debug")]
22+
pub async fn list_deployments(
23+
client: &JsonRpcClient,
24+
registry: AccountId,
25+
count: Option<u32>,
26+
offset: Option<u32>,
27+
) -> RpcResult<Vec<AccountId>> {
28+
let mut all_deployments = Vec::new();
29+
let page_size = 500;
30+
let mut current_offset = 0;
31+
32+
loop {
33+
let params = json!({
34+
"offset": current_offset,
35+
"count": page_size,
36+
});
37+
38+
let page =
39+
view::<Vec<AccountId>>(client, registry.clone(), "list_deployments", params).await?;
40+
41+
let fetched = page.len();
42+
43+
if fetched == 0 {
44+
break;
45+
}
46+
47+
all_deployments.extend(page);
48+
current_offset += fetched;
49+
50+
if fetched < page_size {
51+
break;
52+
}
53+
}
54+
55+
Ok(all_deployments)
56+
}
57+
58+
#[instrument(skip(client), level = "debug")]
59+
pub async fn list_all_deployments(
60+
client: JsonRpcClient,
61+
registries: Vec<AccountId>,
62+
concurrency: usize,
63+
) -> RpcResult<Vec<AccountId>> {
64+
let all_markets: Vec<AccountId> = futures::stream::iter(registries)
65+
.map(|registry| {
66+
let client = client.clone();
67+
async move { list_deployments(&client, registry, None, None).await }
68+
})
69+
.buffer_unordered(concurrency)
70+
.try_concat()
71+
.await?;
72+
73+
Ok(all_markets)
74+
}
75+
976
#[tokio::main]
1077
async fn main() -> LiquidatorResult {
1178
tracing_subscriber::registry()
@@ -14,11 +81,53 @@ async fn main() -> LiquidatorResult {
1481
.init();
1582

1683
let args = Args::parse();
84+
let client = JsonRpcClient::connect(args.network.rpc_url());
85+
let signer = Arc::new(InMemorySigner::from_secret_key(
86+
args.signer_account.clone(),
87+
args.signer_key.clone(),
88+
));
89+
let swap = match args.swap {
90+
SwapType::RheaSwap => Arc::new(RheaSwap::new(
91+
args.swap.account_id(args.network),
92+
client.clone(),
93+
signer.clone(),
94+
)),
95+
};
96+
let asset = Arc::new(args.asset);
1797

18-
let liquidators = setup_liquidators(&args)?;
98+
let registry_refresh_interval = Duration::from_secs(args.registry_refresh_interval);
99+
let mut next_refresh = Instant::now();
100+
let mut markets = HashMap::<AccountId, Liquidator<_>>::new();
19101

20102
loop {
21-
for liquidator in &liquidators {
103+
if Instant::now() >= next_refresh {
104+
info!("Refreshing registry deployments");
105+
let all_markets =
106+
list_all_deployments(client.clone(), args.registries.clone(), args.concurrency)
107+
.await
108+
.map_err(LiquidatorError::ListDeploymentsError)?;
109+
info!("Found {} deployments", all_markets.len());
110+
markets = all_markets
111+
.into_iter()
112+
.map(|market| {
113+
let liquidator = Liquidator::new(
114+
// All clones are Arcs so this is cheap
115+
client.clone(),
116+
signer.clone(),
117+
asset.clone(),
118+
// This is the only true clone
119+
market.clone(),
120+
swap.clone(),
121+
args.timeout,
122+
);
123+
(market, liquidator)
124+
})
125+
.collect();
126+
next_refresh = Instant::now() + registry_refresh_interval;
127+
}
128+
129+
for (market, liquidator) in &markets {
130+
info!("Running liquidations for market: {}", market);
22131
liquidator.run_liquidations(args.concurrency).await?;
23132
}
24133

0 commit comments

Comments
 (0)