Skip to content

Commit

Permalink
Adapt keeper to the latest Slender version
Browse files Browse the repository at this point in the history
  • Loading branch information
artur-abliazimov authored Feb 27, 2024
2 parents 216da32 + 2ca2b67 commit 4839007
Show file tree
Hide file tree
Showing 10 changed files with 318 additions and 358 deletions.
9 changes: 5 additions & 4 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ git clone https://github.com/eq-lab/slender-keeper.git
cd slender-keeper
yarn install
```
3. **Configuration**: Configure the service by setting envs (see below) or by updating the necessary parameters in the `src/consts.ts` file.
3. **Configuration**: Configure the service by setting envs (see below) or by updating the necessary parameters in the `src/configuration.ts` file.
4. **Running the Service**: Start the service by running the following command:
```bash
yarn start
Expand All @@ -26,14 +26,15 @@ This service is designed to automate the process of monitoring of borrowers posi
- If the liquidator has sufficient balances, it initiates the liquidation process for the borrower.
- The service handles liquidation errors and updates the database with borrowers accordingly.
## Configuration
To configure the service for your specific environment, you'll need to set the following env or set values in the `src/consts.ts` file:
`CONTRACT_CREATION_LEDGER` - ledget at which Slender pool was created\
To configure the service for your specific environment, you'll need to set the following env or set values in the `src/configuration.ts` file:
`CONTRACT_CREATION_LEDGER` - ledger number at which Slender pool was created\
`POOL_ID` - Slender pool address\
`POOL_ASSETS` - comma separated list of pool asset addresses\
`XLM_NATIVE` - address of XLM contract\
`SOROBAN_URL` - Sorban RPC URL\
`HORIZON_URL` - Horizon RPC URL\
`NETWORK_PASSPHRASE` - Soroban passphrase\
`LIQUIDATOR_ADDRESS` - liquidator's account address\
`LIQUIDATOR_SECRET` - liquidator's secret key
`LIQUIDATOR_SECRET` - liquidator's secret key\
## License
This project is licensed under the MIT License - see the LICENSE file for details.
6 changes: 2 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@
"main": "dist/index.js",
"dependencies": {
"better-sqlite3": "^8.6.0",
"bigint-conversion": "^2.4.1",
"soroban-client": "1.0.0-beta.2",
"stellar-base": "10.0.0-beta.1",
"stellar-sdk": "11.0.0-beta.3"
"bigint-conversion": "^2.4.3",
"@stellar/stellar-sdk": "^11.2.1"
},
"scripts": {
"start": "yarn build & node dist/index.js",
Expand Down
7 changes: 4 additions & 3 deletions src/consts.ts → src/configuration.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
export const POOL_PRECISION_FACTOR = 1_000_000_000;
export const CONTRACT_CREATION_LEDGER = process.env.CONTRACT_CREATION_LEDGER || 849500;
export const POOL_ID = process.env.POOL_ID || "CAVSCGJKXNS5UW25N4FOC647I2GNQ66N47FND55L6TMDQ7OU5LIMDKGH";
export const SOROBAN_URL = process.env.SOROBAN_URL || "https://rpc-futurenet.stellar.org:443";
export const CONTRACT_CREATION_LEDGER = process.env.CONTRACT_CREATION_LEDGER || 753012;
export const POOL_ID = process.env.POOL_ID || "CCR254VF53IMGX36QVN4ZJOKR6GK3KQJD6BJISX7LE7TXPKQEUV3MFUB";
export const SOROBAN_URL = process.env.SOROBAN_URL || "https://rpc-futurenet.stellar.org";
export const HORIZON_URL = process.env.HORIZON_URL || "https://horizon-futurenet.stellar.org";
export const NETWORK_PASSPHRASE = process.env.NETWORK_PASSPHRASE || "Test SDF Future Network ; October 2022";
export const LIQUIDATOR_ADDRESS = process.env.LIQUIDATOR_ADDRESS;
export const LIQUIDATOR_SECRET = process.env.LIQUIDATOR_SECRET;
export const POOL_ASSETS = process.env.POOL_ASSETS || "CB3VNKT7UEAHHETRHPC3XEAE3SRSVIASUG3P6KG5JFVY6Q5SVISJH2EJ,CC3OEW3BQUUMRWGPDKYESZAOXEOPBLDHKMZR2JYNHR23LIF2ULQVCAUG,CB2O6IY6EVBWFCFKAI2FNWTAYOB4RASUTYPC6VWKQ6IN44VASBQOMWKY";
205 changes: 110 additions & 95 deletions src/contracts.ts
Original file line number Diff line number Diff line change
@@ -1,119 +1,134 @@
import { Address, Contract, Server, xdr, scValToBigInt, TransactionBuilder, BASE_FEE, TimeoutInfinite, Keypair, SorobanRpc } from "soroban-client";
import { Address, BASE_FEE, Contract, Keypair, SorobanRpc, TimeoutInfinite, TransactionBuilder, xdr } from "@stellar/stellar-sdk";
import { promisify } from "util";

import { PoolAccountPosition, PoolReserveData, ReserveData } from "./types";
import { parseScvToJs } from "./parseScvToJs";
import { LIQUIDATOR_ADDRESS, LIQUIDATOR_SECRET, NETWORK_PASSPHRASE, POOL_ID, POOL_PRECISION_FACTOR } from "./consts";

export const getInstanceStorage = async (server: Server, contractId: string) => {
const ledgerKey = xdr.LedgerKey.contractData(
new xdr.LedgerKeyContractData({
contract: new Contract(contractId).address().toScAddress(),
key: xdr.ScVal.scvLedgerKeyContractInstance(),
durability: xdr.ContractDataDurability.persistent()
})
);
const poolInstanceLedgerEntriesRaw = await server.getLedgerEntries(ledgerKey);
const poolInstanceLedgerEntries = xdr.LedgerEntryData.fromXDR(poolInstanceLedgerEntriesRaw.entries[0].xdr, "base64");
return (poolInstanceLedgerEntries.value() as any).body().value().val().value().storage();
import { LIQUIDATOR_ADDRESS, LIQUIDATOR_SECRET, NETWORK_PASSPHRASE, POOL_ASSETS, POOL_ID } from "./configuration";
import { convertScvToJs } from "./parseScvToJs";

export async function getReserves(rpc: SorobanRpc.Server): Promise<ReserveData[]> {
const reserves = POOL_ASSETS
.split(",")
.map(asset => getReserve(rpc, asset)
.then((r) => ({ asset: asset, reserve: r }))
.catch(() => undefined));

const reserveData = (await Promise.all(reserves))
.filter((t) => !!t && t.reserve.reserve_type[0] === "Fungible")
.map<ReserveData>(r => ({ asset: r.asset, debt_token: r.reserve.reserve_type[2] }));

return reserveData;
}

export const getReserves = async (server: Server) => {
const poolInstanceStorageEntries = await getInstanceStorage(server, POOL_ID);
const reserves = new Map<string, ReserveData>();
const reservesReverseMap = {};
const getDefaultReserve = () => ({ debtToken: "", sTokenUnderlyingBalance: 0n });
for (let i = 0; i < poolInstanceStorageEntries.length; i++) {
const key = parseScvToJs(poolInstanceStorageEntries[i].key());
if (key[0] === "ReserveAssetKey") {
const token = key[1];
const value = parseScvToJs(poolInstanceStorageEntries[i].val()) as PoolReserveData;
const { debt_token_address: debtToken, s_token_address: sToken } = value;
const reserve = reserves.get(token) || getDefaultReserve();
reserve.debtToken = debtToken;
reservesReverseMap[sToken] = { lpTokenType: "sToken", token };
reserves.set(token, reserve);
}
}
// exceeded-limit-fix
for (let i = 0; i < poolInstanceStorageEntries.length; i++) {
const key = parseScvToJs(poolInstanceStorageEntries[i].key());
if (key[0] === "STokenUnderlyingBalance") {
const sToken = key[1];
const value = parseScvToJs(poolInstanceStorageEntries[i].val()) as bigint;
const { token } = reservesReverseMap[sToken];
const reserve = reserves.get(token);
reserve.sTokenUnderlyingBalance = value;
reserves.set(token, reserve);
}
}
export async function getBalance(rpc: SorobanRpc.Server, token: string, user: string): Promise<bigint> {
return simulateTransaction(rpc, token, "balance", Address.fromString(user).toScVal());
}

return reserves;
export async function getReserve(rpc: SorobanRpc.Server, asset: string): Promise<PoolReserveData> {
return simulateTransaction(rpc, POOL_ID, "get_reserve", Address.fromString(asset).toScVal());
}

export const getPrice = async (server: Server, priceFeed: string, token: string) => {
const priceFeedInstanceStorageEntries = await getInstanceStorage(server, priceFeed);
for (let i = 0; i < priceFeedInstanceStorageEntries.length; i++) {
const key = parseScvToJs(priceFeedInstanceStorageEntries[i].key());
if (key[0] === "Price" && key[1] === token) {
const value = scValToBigInt(priceFeedInstanceStorageEntries[i].val());
return value;
}
}
export async function getAccountPosition(rpc: SorobanRpc.Server, user: string): Promise<PoolAccountPosition> {
return simulateTransaction(rpc, POOL_ID, "account_position", Address.fromString(user).toScVal());
}

export const getBalance = async (server: Server, token: string, user: string): Promise<bigint> =>
simulateTransaction(server, token, "balance", Address.fromString(user).toScVal());
export async function getDebtCoeff(rpc: SorobanRpc.Server, token: string): Promise<bigint> {
return simulateTransaction(rpc, POOL_ID, "debt_coeff", new Contract(token).address().toScVal())
}

export const getAccountPosition = async (server: Server, user: string): Promise<PoolAccountPosition> =>
simulateTransaction(server, POOL_ID, "account_position", Address.fromString(user).toScVal());
export async function liquidate(rpc: SorobanRpc.Server, who: string): Promise<void> {
return call(rpc, POOL_ID, "liquidate", Address.fromString(LIQUIDATOR_ADDRESS).toScVal(), Address.fromString(who).toScVal(), xdr.ScVal.scvBool(false));
}

export const getDebtCoeff = async (server: Server, token: string): Promise<bigint> =>
simulateTransaction(server, token, "debt_coeff", new Contract(token).address().toScVal())
async function simulateTransaction<T>(
rpc: SorobanRpc.Server,
contractAddress: string,
method: string,
...args: xdr.ScVal[]
): Promise<T> {
const caller = await rpc.getAccount(LIQUIDATOR_ADDRESS);
const contract = new Contract(contractAddress);

export const liquidate = async (server: Server, who: string, token: string) => {
const account = await server.getAccount(LIQUIDATOR_ADDRESS);
const contract = new Contract(POOL_ID);
const operation = new TransactionBuilder(account, {
const transaction = new TransactionBuilder(caller, {
fee: BASE_FEE,
networkPassphrase: NETWORK_PASSPHRASE,
}).addOperation(contract.call(
"liquidate",
Address.fromString(LIQUIDATOR_ADDRESS).toScVal(),
Address.fromString(who).toScVal(),
Address.fromString(token).toScVal(),
xdr.ScVal.scvBool(false))
)
})
.addOperation(contract.call(method, ...(args ?? [])))
.setTimeout(TimeoutInfinite)
.build();
const transaction = await server.prepareTransaction(
operation,
process.env.PASSPHRASE);
transaction.sign(Keypair.fromSecret(LIQUIDATOR_SECRET));
return server.sendTransaction(transaction);
}

export const getCompoundedDebt = async (server: Server, who: string, debtToken: string, debtCoeff: bigint): Promise<bigint> => {
const debtTokenBalance = await getBalance(server, debtToken, who);
return (debtCoeff * debtTokenBalance) / BigInt(POOL_PRECISION_FACTOR);
const simulated = await rpc.simulateTransaction(transaction);

if (SorobanRpc.Api.isSimulationError(simulated)) {
throw new Error(simulated.error);
} else if (!simulated.result) {
throw new Error(`invalid simulation: no result in ${simulated}`);
}

return convertScvToJs<T>(simulated.result.retval);
}

async function simulateTransaction<T>(server: Server, contractAddress: string, call: string, ...args: xdr.ScVal[]): Promise<T> {
const account = await server.getAccount(LIQUIDATOR_ADDRESS);
async function call(
rpc: SorobanRpc.Server,
contractAddress: string,
method: string,
...args: xdr.ScVal[]
): Promise<void> {
const callerKeys = Keypair.fromSecret(LIQUIDATOR_SECRET);

const caller = await rpc.getAccount(callerKeys.publicKey());
const contract = new Contract(contractAddress);
const transaction = new TransactionBuilder(account, {

const operation = new TransactionBuilder(caller, {
fee: BASE_FEE,
networkPassphrase: NETWORK_PASSPHRASE,
}).addOperation(contract.call(call, ...args))
})
.addOperation(contract.call(method, ...(args ?? [])))
.setTimeout(TimeoutInfinite)
.build();

return server.simulateTransaction(transaction)
.then(simulated => {
if (SorobanRpc.isSimulationError(simulated)) {
throw new Error(simulated.error);
} else if (!simulated.result) {
throw new Error(`invalid simulation: no result in ${simulated}`);
}

return parseScvToJs(simulated.result.retval)
});
}
const simulated = (await rpc.simulateTransaction(
operation,
)) as SorobanRpc.Api.SimulateTransactionSuccessResponse;

if (SorobanRpc.Api.isSimulationError(simulated)) {
throw new Error(simulated.error);
} else if (!simulated.result) {
throw new Error(`Invalid simulation: no result in ${simulated}`);
}

const transaction = SorobanRpc.assembleTransaction(operation, simulated).build();

transaction.sign(callerKeys);

const response = await rpc.sendTransaction(transaction);

let result: SorobanRpc.Api.GetTransactionResponse;
let attempts = 15;

if (response.status == 'ERROR') {
throw Error(`Failed to send transaction: ${response.errorResult.toXDR('base64')}`);
}

do {
await delay(1000);
result = await rpc.getTransaction(response.hash);
attempts--;
} while (result.status === SorobanRpc.Api.GetTransactionStatus.NOT_FOUND && attempts > 0);

if (result.status == SorobanRpc.Api.GetTransactionStatus.NOT_FOUND) {
throw Error('Submitted transaction was not found');
}

if ('resultXdr' in result) {
const getResult = result as SorobanRpc.Api.GetTransactionResponse;
if (getResult.status !== SorobanRpc.Api.GetTransactionStatus.SUCCESS) {
throw new Error('Transaction result is insuccessfull');
} else {
return;
}
}

throw Error(`Transaction failed (method: ${method})`);
}

export let delay = promisify((ms, res) => setTimeout(res, ms))
2 changes: 1 addition & 1 deletion src/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const updateLastSyncedLedger = (lastSyncedLedger: number) => {
db.prepare('UPDATE ledger SET last_synced=(?) WHERE rowid=1').run(lastSyncedLedger);
}

export const readBorrowers = () => db.prepare('SELECT borrower from borrowers').get() || []
export const readBorrowers = () => db.prepare('SELECT borrower from borrowers').all() || []

export const insertBorrowers = (borrowers: string[]) => {
for (const borrower of borrowers) {
Expand Down
Loading

0 comments on commit 4839007

Please sign in to comment.