diff --git a/services/delegator/src/Borrower.ts b/services/delegator/src/Borrower.ts index f30146f..6dec547 100644 --- a/services/delegator/src/Borrower.ts +++ b/services/delegator/src/Borrower.ts @@ -2,7 +2,14 @@ import Web3 from 'web3'; import { Big } from '@goldenagellc/web3-blocks'; -import { CTokenSymbol, cTokenSymbols } from './types/CTokens'; +import { + CTokens, + CTokenSymbol, + cTokenSymbols, + CTokenVersion, + CTokenVersions, + CTokenUnderlyingDecimals as decimals, +} from './types/CTokens'; import { CToken } from './contracts/CToken'; import PriceLedger from './PriceLedger'; import StatefulComptroller from './StatefulComptroller'; @@ -20,6 +27,15 @@ interface ILiquidity { edges: ('min' | 'max')[]; } +interface ILiquidationInformation { + health: Big; + repayCToken: CTokens; + seizeCToken: CTokens; + revenueETH: Big; + symbols: CTokenSymbol[]; + edges: ('min' | 'max')[]; +} + export default class Borrower { public readonly address: string; protected readonly positions: { readonly [_ in CTokenSymbol]: IBorrowerPosition }; @@ -43,10 +59,10 @@ export default class Borrower { console.error(`Failed to get account snapshot for ${this.address}: ${snapshot.error}`); return false; } - + const position = this.positions[symbol]; if (position.borrowIndex.eq('0')) { - console.error(`${this.address} invalud due to 0 borrow index`); + console.error(`${this.address} invalid due to 0 borrow index`); return false; } const supply = position.supply; @@ -87,7 +103,7 @@ export default class Borrower { exchangeRates: { [_ in CTokenSymbol]: Big }, borrowIndices: { [_ in CTokenSymbol]: Big }, ): ILiquidity | null { - let collat: Big = new Big('0'); + let supply: Big = new Big('0'); let borrow: Big = new Big('0'); const symbols: CTokenSymbol[] = []; const edges: ('min' | 'max')[] = []; @@ -101,16 +117,21 @@ export default class Borrower { if (collateralFactor === null || pricesUSD.min === null || pricesUSD.max === null) return null; const edge: 'min' | 'max' = position.supply.gt('0') ? 'min' : 'max'; - collat = collat.plus( + supply = supply.plus( position.supply .times(exchangeRates[symbol]) .div('1e+18') .times(collateralFactor) .div('1e+18') - .times(pricesUSD[edge]!), + .times(pricesUSD[edge]!) + .div(`1e+${decimals[symbol]}`), ); borrow = borrow.plus( - position.borrow.times(borrowIndices[symbol]).div(position.borrowIndex).times(pricesUSD[edge]!), + position.borrow + .times(borrowIndices[symbol]) + .div(position.borrowIndex) + .times(pricesUSD[edge]!) + .div(`1e+${decimals[symbol]}`), ); symbols.push(symbol); edges.push(edge); @@ -118,12 +139,12 @@ export default class Borrower { let liquidity: Big; let shortfall: Big; - if (collat.gt(borrow)) { - liquidity = collat.minus(borrow); + if (supply.gt(borrow)) { + liquidity = supply.minus(borrow); shortfall = new Big('0'); } else { liquidity = new Big('0'); - shortfall = borrow.minus(collat); + shortfall = borrow.minus(supply); } return { @@ -133,4 +154,135 @@ export default class Borrower { edges: edges, }; } + + public expectedRevenue( + comptroller: StatefulComptroller, + priceLedger: PriceLedger, + exchangeRates: { [_ in CTokenSymbol]: Big }, + borrowIndices: { [_ in CTokenSymbol]: Big }, + ): ILiquidationInformation | null { + const closeFactor = comptroller.getCloseFactor(); + const liquidationIncentive = comptroller.getLiquidationIncentive(); + if (closeFactor === null || liquidationIncentive === null) { + console.log('Borrower computation error: closeFactor|liquidationIncentive === null'); + return null; + } + + let supplyTotal: Big = new Big('0'); // total available borrow + let borrowTotal: Big = new Big('0'); // utilized borrow + const symbols: CTokenSymbol[] = []; + const edges: ('min' | 'max')[] = []; + + // top2_____Assets will contain symbols. + // idx 0 is the best, and idx 1 is the second best + let top2RepayAssets: (CTokenSymbol | null)[] = [null, null]; + let top2SeizeAssets: (CTokenSymbol | null)[] = [null, null]; + // top2_____Assets will contain amounts (in USD) corresponding + // to the top2_____Assets + let top2RepayAmounts: Big[] = [new Big('0'), new Big('0')]; + let top2SeizeAmounts: Big[] = [new Big('0'), new Big('0')]; + + for (let symbol of cTokenSymbols) { + // retrieve position and ensure it's valid + const position = this.positions[symbol]; + if (position.supply.eq('0') && position.borrow.eq('0')) continue; + if (position.borrow.gt('0') && position.borrowIndex.eq('0')) continue; + + // retrieve collateral factor, min price, and max price for this symbol + const collateralFactor = comptroller.getCollateralFactor(symbol); + const pricesUSD = priceLedger.getPrices(symbol); + if (collateralFactor === null || pricesUSD.min === null || pricesUSD.max === null) { + console.log('Borrower computation error: collateralFactor|price.min|price.max === null'); + continue; + } + + // liquidity calculations + const edge: 'min' | 'max' = position.supply.gt('0') ? 'min' : 'max'; + const supply = position.supply.gt('0') + ? position.supply + .times(exchangeRates[symbol]) // 18 extra + .div('1e+18') // 0 extra (now in units of underlying) + .times(collateralFactor) // 18 extra + .div('1e+18') // 0 extra (still in units of underlying) + .times(pricesUSD[edge]!) // now in USD, with (6 + N) decimals + .div(`1e+${decimals[symbol]}`) + : new Big('0'); + const borrow = position.borrow.gt('0') + ? position.borrow + .times(borrowIndices[symbol]) + .div(position.borrowIndex) + .times(pricesUSD[edge]!) + .div(`1e+${decimals[symbol]}`) + : new Big('0'); + + // revenue calculations + const seize = supply.times('1e+18').div(liquidationIncentive); + const repay = borrow.times(closeFactor).div('1e+18'); + + // update outer liquidity variables + supplyTotal = supplyTotal.plus(supply); + borrowTotal = borrowTotal.plus(borrow); + symbols.push(symbol); + edges.push(edge); + + // update outer revenue variables... + // ...repay + if (top2RepayAmounts[0].lt(repay)) { + top2RepayAmounts = [repay, top2RepayAmounts[0]]; + top2RepayAssets = [symbol, top2RepayAssets[0]]; + } else if (top2RepayAmounts[1].lt(repay)) { + top2RepayAmounts[1] = repay; + top2RepayAssets[1] = symbol; + } + // ...seize + if (top2SeizeAmounts[0].lt(seize)) { + top2SeizeAmounts = [seize, top2SeizeAmounts[0]]; + top2SeizeAssets = [symbol, top2SeizeAssets[0]]; + } else if (top2SeizeAmounts[1].lt(seize)) { + top2SeizeAmounts[1] = seize; + top2SeizeAssets[1] = symbol; + } + } + + let repayCToken: CTokens | null = null; + let seizeCToken: CTokens | null = null; + let revenue = new Big('0'); + + if (top2RepayAssets[0] !== null && top2SeizeAssets[0] !== null) { + const ableToPickBest = + top2RepayAssets[0] !== top2SeizeAssets[0] || CTokenVersions[top2RepayAssets[0]] === CTokenVersion.V2; + + const repayIdx = Number(!ableToPickBest && top2RepayAmounts[1].gt(top2SeizeAmounts[1])); + const seizeIdx = Number(ableToPickBest ? false : !repayIdx); + + if (top2RepayAssets[repayIdx] !== null && top2SeizeAssets[seizeIdx] !== null) { + repayCToken = CTokens[top2RepayAssets[repayIdx]!]; + seizeCToken = CTokens[top2SeizeAssets[seizeIdx]!]; + const repayAmount = top2RepayAmounts[repayIdx]; + const seizeAmount = top2SeizeAmounts[seizeIdx]; + + if (repayAmount.lt(seizeAmount)) { + revenue = repayAmount.times(liquidationIncentive.minus('1e+18')).div('1e+18'); + } else { + revenue = seizeAmount.times(liquidationIncentive.minus('1e+18')).div('1e+18'); + } + + const priceETH = priceLedger.getPrices('cETH').min; + return { + health: supplyTotal.div(borrowTotal), + repayCToken: repayCToken, + seizeCToken: seizeCToken, + revenueETH: priceETH === null ? new Big('0') : revenue.times('1e+6').div(priceETH), + symbols: symbols, + edges: edges, + }; + } else { + // console.log('Borrower computation error: only has one asset and not v2'); + return null; + } + } else { + console.log('Borrower computation error: repay or seize assets === null'); + return null; + } + } } diff --git a/services/delegator/src/CoinbaseReporter.ts b/services/delegator/src/CoinbaseReporter.ts index 9ad1bf8..4f527c6 100644 --- a/services/delegator/src/CoinbaseReporter.ts +++ b/services/delegator/src/CoinbaseReporter.ts @@ -17,6 +17,7 @@ interface LocalSignature { } interface CoinbaseReport { + message?: string, timestamp: string; messages: string[]; signatures: string[]; diff --git a/services/delegator/src/PriceLedger.ts b/services/delegator/src/PriceLedger.ts index 7787a82..350327f 100644 --- a/services/delegator/src/PriceLedger.ts +++ b/services/delegator/src/PriceLedger.ts @@ -100,7 +100,7 @@ export default class PriceLedger { formatted.messages.push(postableData.message); formatted.signatures.push(postableData.signature); - formatted.signatures.push(postableData.key); // should equal local `key` + formatted.symbols.push(postableData.key); // should equal local `key` }); if (didFindNull) return null; diff --git a/services/delegator/src/StatefulBorrower.ts b/services/delegator/src/StatefulBorrower.ts index d7a89be..2232d44 100644 --- a/services/delegator/src/StatefulBorrower.ts +++ b/services/delegator/src/StatefulBorrower.ts @@ -2,158 +2,90 @@ import { EventData } from 'web3-eth-contract'; import Web3 from 'web3'; import winston from 'winston'; -import { Big } from '@goldenagellc/web3-blocks'; - import { CTokenReversed, CTokenSymbol, cTokenSymbols } from './types/CTokens'; import { CToken } from './contracts/CToken'; -import Borrower, { IBorrowerPosition } from './Borrower'; +import Borrower from './Borrower'; export default class StatefulBorrower extends Borrower { - protected readonly fetchBlock: number = 0; - private _didInit: boolean = false; + private readonly provider: Web3; + private readonly cTokens: { [_ in CTokenSymbol]: CToken }; - constructor(address: string, fetchBlock: number) { + constructor(address: string, provider: Web3, cTokens: { [_ in CTokenSymbol]: CToken }) { super(address); - this.fetchBlock = fetchBlock; + this.provider = provider; + this.cTokens = cTokens; } - public get didInit(): boolean { - return this._didInit; + public fetchAll(block: number): Promise[] { + return cTokenSymbols.map(async (symbol) => this.fetch(symbol, block)); } - public async init(provider: Web3, cTokens: { [_ in CTokenSymbol]: CToken }): Promise { - if (this._didInit) { - console.warn('Already initialized borrower. Aborting!'); - return; - } - - let didInit = true; - for (let symbol of cTokenSymbols) { - const snapshot = await cTokens[symbol].getAccountSnapshot(this.address)(provider, this.fetchBlock); - if (snapshot.error !== '0') { - didInit = false; - continue; - } + public async fetch(symbol: CTokenSymbol, block: number): Promise { + const snapshot = await this.cTokens[symbol].getAccountSnapshot(this.address)(this.provider, block); + if (snapshot.error !== '0') return; - const position = this.positions[symbol]; - position.supply = position.supply.plus(snapshot.cTokenBalance); - position.borrow = position.borrow.plus(snapshot.borrowBalance); + const borrowIndex = await this.cTokens[symbol].borrowIndex()(this.provider, block); - const borrowIndex = await cTokens[symbol].borrowIndex()(provider, this.fetchBlock); - if (borrowIndex.gt(position.borrowIndex)) position.borrowIndex = borrowIndex; - } - this._didInit = didInit; + const position = this.positions[symbol]; + position.supply = snapshot.cTokenBalance; + position.borrow = snapshot.borrowBalance; + position.borrowIndex = borrowIndex; } - public onMint(event: EventData, undo = false): void { - if (event.blockNumber <= this.fetchBlock) return; - + public onMint(event: EventData): Promise | void { const symbol = this.getSymbolFor(event.address); if (symbol === null) return; - const position = this.positions[symbol]; - if (undo) { - position.supply = position.supply.minus(event.returnValues.mintTokens); - winston.info(`🪙 *${symbol} Mint* by ${this.address.slice(2, 8)} removed from chain`); - } else { - position.supply = position.supply.plus(event.returnValues.mintTokens); - winston.info(`🪙 *${symbol} Mint* by ${this.address.slice(2, 8)}`); - } + winston.info(`🟢 *${symbol} Mint* by ${this.address.slice(2, 8)}`); + return this.fetch(symbol, event.blockNumber); } - public onRedeem(event: EventData, undo = false): void { - if (event.blockNumber <= this.fetchBlock) return; - + public onRedeem(event: EventData): Promise | void { const symbol = this.getSymbolFor(event.address); if (symbol === null) return; - const position = this.positions[symbol]; - if (undo) { - position.supply = position.supply.plus(event.returnValues.redeemTokens); - winston.info(`🪙 *${symbol} Redeem* by ${this.address.slice(2, 8)} removed from chain`); - } else { - position.supply = position.supply.minus(event.returnValues.redeemTokens); - winston.info(`🪙 *${symbol} Redeem* by ${this.address.slice(2, 8)}`); - } + winston.info(`🟢 *${symbol} Redeem* by ${this.address.slice(2, 8)}`); + return this.fetch(symbol, event.blockNumber); } - public onBorrow(event: EventData, undo = false, currentBorrowIndex: Big): void { - if (event.blockNumber <= this.fetchBlock) return; - + public onBorrow(event: EventData): Promise | void { const symbol = this.getSymbolFor(event.address); if (symbol === null) return; - const position = this.positions[symbol]; - if (undo) { - position.borrow = position.borrow.minus(event.returnValues.borrowAmount); - winston.info(`🪙 *${symbol} Borrow* by ${this.address.slice(2, 8)} removed from chain`); - } else { - position.borrow = new Big(event.returnValues.accountBorrows); - position.borrowIndex = currentBorrowIndex; - winston.info(`🪙 *${symbol} Borrow* by ${this.address.slice(2, 8)}`); - } + winston.info(`🔵 *${symbol} Borrow* by ${this.address.slice(2, 8)}`); + return this.fetch(symbol, event.blockNumber); } - public onRepayBorrow(event: EventData, undo = false, currentBorrowIndex: Big): void { - if (event.blockNumber <= this.fetchBlock) return; - + public onRepayBorrow(event: EventData): Promise | void { const symbol = this.getSymbolFor(event.address); if (symbol === null) return; - const position = this.positions[symbol]; - if (undo) { - position.borrow = position.borrow.plus(event.returnValues.repayAmount); - winston.info(`🪙 *${symbol} Repay* by ${this.address.slice(2, 8)} removed from chain`); - } else { - position.borrow = new Big(event.returnValues.accountBorrows); - position.borrowIndex = currentBorrowIndex; - winston.info(`🪙 *${symbol} Repay* by ${this.address.slice(2, 8)}`); - } + winston.info(`🔵 *${symbol} Repay* by ${this.address.slice(2, 8)}`); + return this.fetch(symbol, event.blockNumber); } - public onLiquidateBorrow(event: EventData, undo = false): void { - if (event.blockNumber <= this.fetchBlock) return; - - const symbolA = this.getSymbolFor(event.address); - if (symbolA === null) return; - const positionA = this.positions[symbolA]; - const symbolB = this.getSymbolFor(event.returnValues.cTokenCollateral); - if (symbolB === null) return; - const positionB = this.positions[symbolB]; - if (positionA === null || positionB === null) return; - - if (undo) { - positionA.borrow = positionA.borrow.plus(event.returnValues.repayAmount); - positionB.supply = positionB.supply.plus(event.returnValues.seizeTokens); - winston.info(`💦 Liquidation ${event.transactionHash.slice(0, 10)} removed from chain`); - } else { - positionA.borrow = positionA.borrow.minus(event.returnValues.repayAmount); - positionB.supply = positionB.supply.minus(event.returnValues.seizeTokens); - winston.info( - `💦 ${this.address.slice( - 2, - 8, - )} had their *${symbolA} liquidated* and ${symbolB} seized by ${event.returnValues.liquidator.slice(2, 8)}`, - ); - } + public onLiquidateBorrow(event: EventData, undo = false): Promise | void { + const symbolRepay = this.getSymbolFor(event.address); + const symbolSeize = this.getSymbolFor(event.returnValues.cTokenCollateral); + if (symbolRepay === null || symbolSeize === null) return; + + winston.info( + `🟣 ${this.address.slice( + 2, + 8, + )} had their *${symbolRepay} liquidated* and ${symbolSeize} seized by ${event.returnValues.liquidator.slice( + 2, + 8, + )}`, + ); + return Promise.all([this.fetch(symbolRepay, event.blockNumber), this.fetch(symbolSeize, event.blockNumber)]); } - public onTransfer(event: EventData, undo = false): void { - if (event.blockNumber <= this.fetchBlock) return; - + public onTransfer(event: EventData): Promise | void { const symbol = this.getSymbolFor(event.address); if (symbol === null) return; - const position = this.positions[symbol]; - // Make sure that this borrower is the `to` address, and that this wasn't a Mint/Redeem side-effect - const shouldAdd = this.address === event.returnValues.to && event.address !== event.returnValues.from; - if (shouldAdd) { - if (undo) position.supply = position.supply.minus(event.returnValues.amount); - else position.supply = position.supply.plus(event.returnValues.amount); - } else { - if (undo) position.supply = position.supply.plus(event.returnValues.amount); - else position.supply = position.supply.minus(event.returnValues.amount); - } + return this.fetch(symbol, event.blockNumber); } private getSymbolFor(address: string): CTokenSymbol | null { diff --git a/services/delegator/src/StatefulBorrowers.ts b/services/delegator/src/StatefulBorrowers.ts index 8f8518e..48700b6 100644 --- a/services/delegator/src/StatefulBorrowers.ts +++ b/services/delegator/src/StatefulBorrowers.ts @@ -6,6 +6,9 @@ import { Big } from '@goldenagellc/web3-blocks'; import { CTokenSymbol, cTokenSymbols } from './types/CTokens'; import { CToken } from './contracts/CToken'; import StatefulBorrower from './StatefulBorrower'; +import StatefulComptroller from './StatefulComptroller'; +import PriceLedger from './PriceLedger'; +import ILiquidationCandidate from './types/ILiquidationCandidate'; export default class StatefulBorrowers { private readonly provider: Web3; @@ -40,20 +43,44 @@ export default class StatefulBorrowers { public async push(addresses: string[]): Promise { const block = await this.provider.eth.getBlockNumber(); addresses.forEach((address) => { - this.borrowers[address] = new StatefulBorrower(address, block); - this.borrowers[address].init(this.provider, this.cTokens); + this.borrowers[address] = new StatefulBorrower(address, this.provider, this.cTokens); + this.borrowers[address].fetchAll(block); }); } - public async randomCheck(): Promise { - const keys = Object.keys(this.borrowers); - const borrower = this.borrowers[keys[(keys.length * Math.random()) << 0]]; - if (!borrower.didInit) { - console.log(`${borrower.address} hasn't been initialized yet`); - } else { - const valid = await borrower.verify(this.provider, this.cTokens, this.borrowIndices, 0.01); - if (!valid) console.log(`${borrower.address} has invalid state`); - } + // public async randomCheck(): Promise { + // const keys = Object.keys(this.borrowers); + // const borrower = this.borrowers[keys[(keys.length * Math.random()) << 0]]; + // const valid = await borrower.verify(this.provider, this.cTokens, this.borrowIndices, 0.01); + // if (!valid) console.log(`${borrower.address} has invalid state`); + // } + + public async scan(comptroller: StatefulComptroller, priceLedger: PriceLedger): Promise { + const exchangeRateArray = await Promise.all(this.fetchExchangeRates()); + const exchangeRates = Object.fromEntries(cTokenSymbols.map((symbol, i) => [symbol, exchangeRateArray[i]])) as { + [_ in CTokenSymbol]: Big; + }; + + const candidates: ILiquidationCandidate[] = []; + + Object.keys(this.borrowers).forEach((address) => { + const borrower = this.borrowers[address]; + const info = borrower.expectedRevenue(comptroller, priceLedger, exchangeRates, this.borrowIndices); + + if (info !== null && info.health.lt('1')) { + const postable = priceLedger.getPostableFormat(info.symbols, info.edges); + if (postable === null) return; + candidates.push({ + address: address, + repayCToken: info.repayCToken, + seizeCToken: info.seizeCToken, + pricesToReport: postable, + expectedRevenue: info.revenueETH.div('1e+6').toNumber(), + }); + } + }); + + return candidates; } private fetchBorrowIndices(block: number): Promise[] { @@ -62,6 +89,10 @@ export default class StatefulBorrowers { }); } + private fetchExchangeRates(): Promise[] { + return cTokenSymbols.map((symbol) => this.cTokens[symbol].exchangeRateStored()(this.provider)); + } + private subscribe(block: number): void { cTokenSymbols.forEach((symbol) => { const subscribeTo = this.cTokens[symbol].bindTo(this.provider).subscribeTo; @@ -81,7 +112,7 @@ export default class StatefulBorrowers { }) .on('changed', (ev: EventData) => { const minter: string = ev.returnValues.minter; - if (minter in this.borrowers) this.borrowers[minter].onMint(ev, true); + if (minter in this.borrowers) this.borrowers[minter].onMint(ev); }) .on('error', console.log); @@ -93,7 +124,7 @@ export default class StatefulBorrowers { }) .on('changed', (ev: EventData) => { const redeemer: string = ev.returnValues.redeemer; - if (redeemer in this.borrowers) this.borrowers[redeemer].onRedeem(ev, true); + if (redeemer in this.borrowers) this.borrowers[redeemer].onRedeem(ev); }) .on('error', console.log); @@ -101,11 +132,11 @@ export default class StatefulBorrowers { .Borrow(block) .on('data', (ev: EventData) => { const borrower: string = ev.returnValues.borrower; - if (borrower in this.borrowers) this.borrowers[borrower].onBorrow(ev, false, this.borrowIndices[symbol]); + if (borrower in this.borrowers) this.borrowers[borrower].onBorrow(ev); }) .on('changed', (ev: EventData) => { const borrower: string = ev.returnValues.borrower; - if (borrower in this.borrowers) this.borrowers[borrower].onBorrow(ev, true, this.borrowIndices[symbol]); + if (borrower in this.borrowers) this.borrowers[borrower].onBorrow(ev); }) .on('error', console.log); @@ -113,11 +144,11 @@ export default class StatefulBorrowers { .RepayBorrow(block) .on('data', (ev: EventData) => { const borrower: string = ev.returnValues.borrower; - if (borrower in this.borrowers) this.borrowers[borrower].onRepayBorrow(ev, false, this.borrowIndices[symbol]); + if (borrower in this.borrowers) this.borrowers[borrower].onRepayBorrow(ev); }) .on('changed', (ev: EventData) => { const borrower: string = ev.returnValues.borrower; - if (borrower in this.borrowers) this.borrowers[borrower].onRepayBorrow(ev, true, this.borrowIndices[symbol]); + if (borrower in this.borrowers) this.borrowers[borrower].onRepayBorrow(ev); }) .on('error', console.log); @@ -129,7 +160,7 @@ export default class StatefulBorrowers { }) .on('changed', (ev: EventData) => { const borrower: string = ev.returnValues.borrower; - if (borrower in this.borrowers) this.borrowers[borrower].onLiquidateBorrow(ev, true); + if (borrower in this.borrowers) this.borrowers[borrower].onLiquidateBorrow(ev); }) .on('error', console.log); @@ -143,9 +174,9 @@ export default class StatefulBorrowers { }) .on('changed', (ev: EventData) => { const from: string = ev.returnValues.from; - if (from in this.borrowers) this.borrowers[from].onTransfer(ev, true); + if (from in this.borrowers) this.borrowers[from].onTransfer(ev); const to: string = ev.returnValues.to; - if (to in this.borrowers) this.borrowers[to].onTransfer(ev, true); + if (to in this.borrowers) this.borrowers[to].onTransfer(ev); }) .on('error', console.log); }); diff --git a/services/delegator/src/StatefulPricesCoinbase.ts b/services/delegator/src/StatefulPricesCoinbase.ts index 1b77232..d8ba857 100644 --- a/services/delegator/src/StatefulPricesCoinbase.ts +++ b/services/delegator/src/StatefulPricesCoinbase.ts @@ -40,8 +40,9 @@ export default class StatefulPricesCoinbase extends CoinbaseReporter { const updatedKeys: CoinbaseKey[] = []; const report = await this.fetchCoinbasePrices(); - if (report.messages === undefined) { - console.log(report); + if (report.message === 'request timestamp expired') { + console.log('Coinbase fetch failed -- request timestamp outdated'); + return []; } for (let i = 0; i < report.messages.length; i += 1) { const message = report.messages[i]; @@ -58,7 +59,7 @@ export default class StatefulPricesCoinbase extends CoinbaseReporter { } return updatedKeys; } catch (e) { - if (e instanceof FetchError) console.log('Coinbase fetch failed. Connection probably timed out'); + if (e instanceof FetchError) console.log('Coinbase fetch failed -- probably lost internet'); else console.log(e); return []; } diff --git a/services/delegator/src/contracts/CToken.ts b/services/delegator/src/contracts/CToken.ts index 858e86c..c06a598 100644 --- a/services/delegator/src/contracts/CToken.ts +++ b/services/delegator/src/contracts/CToken.ts @@ -2,7 +2,7 @@ import Web3Utils from 'web3-utils'; import { Big, BindableContract, ContractCaller } from '@goldenagellc/web3-blocks'; -import { CTokens, CTokenSymbol, CTokenCreationBlocks } from '../types/CTokens'; +import { CTokens, CTokenSymbol, CTokenVersion, CTokenCreationBlocks, CTokenVersions } from '../types/CTokens'; import abiEth from './abis/cether.json'; import abiV1 from './abis/ctokenv1.json'; @@ -26,8 +26,25 @@ export enum CTokenEvents { } export class CToken extends BindableContract { - constructor(address: string, abi: any, creationBlock: number) { - super(address, abi as Web3Utils.AbiItem[], CTokenEvents, creationBlock); + public readonly symbol: CTokenSymbol; + public readonly version: CTokenVersion; + + constructor(symbol: CTokenSymbol) { + let abi: Web3Utils.AbiItem[]; + switch (CTokenVersions[symbol]) { + case CTokenVersion.V1: + abi = abiV1 as Web3Utils.AbiItem[]; + break; + case CTokenVersion.V2: + abi = abiV2 as Web3Utils.AbiItem[]; + break; + case CTokenVersion.ETH: + abi = abiEth as Web3Utils.AbiItem[]; + break; + } + super(CTokens[symbol], abi, CTokenEvents, CTokenCreationBlocks[symbol]); + this.symbol = symbol; + this.version = CTokenVersions[symbol]; } public exchangeRateStored(): ContractCaller { @@ -66,17 +83,17 @@ export class CToken extends BindableContract { type InstanceMap = { [_ in CTokenSymbol]: T }; const cTokens: InstanceMap = { - cBAT: new CToken(CTokens.cBAT, abiV1, CTokenCreationBlocks.cBAT), - cCOMP: new CToken(CTokens.cCOMP, abiV2, CTokenCreationBlocks.cCOMP), - cDAI: new CToken(CTokens.cDAI, abiV2, CTokenCreationBlocks.cDAI), - cETH: new CToken(CTokens.cETH, abiEth, CTokenCreationBlocks.cETH), - cREP: new CToken(CTokens.cREP, abiV1, CTokenCreationBlocks.cREP), - cSAI: new CToken(CTokens.cSAI, abiV1, CTokenCreationBlocks.cSAI), - cUNI: new CToken(CTokens.cUNI, abiV2, CTokenCreationBlocks.cUNI), - cUSDC: new CToken(CTokens.cUSDC, abiV1, CTokenCreationBlocks.cUSDC), - cUSDT: new CToken(CTokens.cUSDT, abiV2, CTokenCreationBlocks.cUSDT), - cWBTC: new CToken(CTokens.cWBTC, abiV1, CTokenCreationBlocks.cWBTC), - cZRX: new CToken(CTokens.cZRX, abiV1, CTokenCreationBlocks.cZRX), + cBAT: new CToken('cBAT'), + cCOMP: new CToken('cCOMP'), + cDAI: new CToken('cDAI'), + cETH: new CToken('cETH'), + cREP: new CToken('cREP'), + cSAI: new CToken('cSAI'), + cUNI: new CToken('cUNI'), + cUSDC: new CToken('cUSDC'), + cUSDT: new CToken('cUSDT'), + cWBTC: new CToken('cWBTC'), + cZRX: new CToken('cZRX'), }; export default cTokens; diff --git a/services/delegator/src/start.ts b/services/delegator/src/start.ts index ca8e6ea..977ad25 100644 --- a/services/delegator/src/start.ts +++ b/services/delegator/src/start.ts @@ -1,5 +1,4 @@ import ipc from 'node-ipc'; -import { EventData } from 'web3-eth-contract'; import Web3Utils from 'web3-utils'; import winston from 'winston'; @@ -61,7 +60,7 @@ const statefulPricesCoinbase = new StatefulPricesCoinbase( process.env.CB_ACCESS_PASSPHRASE!, ); -async function start() { +async function start(ipc: any) { await statefulBorrowers.init(); await statefulComptroller.init(); await statefulPricesOnChain.init(); @@ -73,36 +72,27 @@ async function start() { statefulBorrowers.push(borrowers.map((x) => Web3Utils.toChecksumAddress(x))); - setInterval(statefulBorrowers.randomCheck.bind(statefulBorrowers), 500); + setInterval(async () => { + const candidates = await statefulBorrowers.scan(statefulComptroller, priceLedger); + candidates.forEach((candidate) => { + ipc.emit('liquidation-candidate-add', candidate); + }); + }, 4000); + // setInterval(() => provider.eth.isSyncing((e, s) => console.log(s)), 1000); } -// const borrowers: ICompoundBorrower[] = []; -// addressesList.forEach(address => { -// cTokens.cBAT.getAccountSnapshot -// }); +ipc.config.appspace = 'newbedford.'; +ipc.config.id = 'delegator'; +ipc.config.silent = true; +ipc.connectTo('txmanager', '/tmp/newbedford.txmanager', () => { + ipc.of['txmanager'].on('connect', () => { + console.log('Connected'); -start(); + start(ipc.of['txmanager']); -// symbols.forEach((symbol) => { -// const accrueInterestEmitter = cTokens[symbol].bindTo(provider).subscribeTo.AccrueInterest('latest'); - -// accrueInterestEmitter -// .on('connected', (id: string) => console.log(`Connected ${symbol} at ${id}`)) -// .on('data', console.log) -// .on('changed', console.log) -// .on('error', console.log); -// }); - -// ipc.config.appspace = 'newbedford.'; -// ipc.config.id = 'delegator'; -// // ipc.config.silent = true; -// ipc.connectTo('txmanager', '/tmp/newbedford.txmanager', () => { -// ipc.of['txmanager'].on('connect', () => { -// console.log('Connected'); - -// ipc.of['txmanager'].emit('liquidation-candidate-add', 'My message'); -// }); -// }); + // ipc.of['txmanager'].emit('liquidation-candidate-add', 'My message'); + }); +}); process.on('SIGINT', () => { console.log('\nCaught interrupt signal'); @@ -112,6 +102,8 @@ process.on('SIGINT', () => { // @ts-expect-error: We already checked that type is valid provider.eth.currentProvider.connection.destroy(); + ipc.disconnect('txmanager'); + console.log('Exited cleanly'); process.exit(); }); diff --git a/services/delegator/src/types/CTokens.ts b/services/delegator/src/types/CTokens.ts index 14b0031..5acf1d7 100644 --- a/services/delegator/src/types/CTokens.ts +++ b/services/delegator/src/types/CTokens.ts @@ -14,6 +14,12 @@ export enum CTokens { cZRX = '0xB3319f5D18Bc0D84dD1b4825Dcde5d5f7266d407', } +export enum CTokenVersion { + V1, + V2, + ETH, +} + export type CTokenSymbol = keyof typeof CTokens; export const cTokenSymbols = Object.keys(CTokens); @@ -45,6 +51,20 @@ export const CTokenUnderlyingDecimals: { [_ in CTokenSymbol]: number } = { cZRX: 18, }; +export const CTokenVersions: { [_ in CTokenSymbol]: CTokenVersion } = { + cBAT: CTokenVersion.V1, + cCOMP: CTokenVersion.V2, + cDAI: CTokenVersion.V2, + cETH: CTokenVersion.ETH, + cREP: CTokenVersion.V1, + cSAI: CTokenVersion.V1, + cUNI: CTokenVersion.V2, + cUSDC: CTokenVersion.V1, + cUSDT: CTokenVersion.V2, + cWBTC: CTokenVersion.V1, + cZRX: CTokenVersion.V1, +}; + export const CTokenCoinbaseKeys: { [_ in CTokenSymbol]: CoinbaseKey | null } = { cBAT: 'BAT', cCOMP: 'COMP', @@ -59,4 +79,6 @@ export const CTokenCoinbaseKeys: { [_ in CTokenSymbol]: CoinbaseKey | null } = { cZRX: 'ZRX', }; -export const CTokenReversed: { [i: string]: CTokenSymbol } = Object.fromEntries(cTokenSymbols.map((symbol) => [CTokens[symbol], symbol])); +export const CTokenReversed: { [i: string]: CTokenSymbol } = Object.fromEntries( + cTokenSymbols.map((symbol) => [CTokens[symbol], symbol]), +); diff --git a/services/txmanager/src/start.ts b/services/txmanager/src/start.ts index 8e83ca5..d71f7cb 100644 --- a/services/txmanager/src/start.ts +++ b/services/txmanager/src/start.ts @@ -5,6 +5,7 @@ import winston from 'winston'; import { ProviderGroup, Wallet } from '@goldenagellc/web3-blocks'; import EthSubscriber from './EthSubscriber'; +import ILiquidationCandidate from './types/ILiquidationCandidate'; import IncognitoQueue from './IncognitoQueue'; import LatencyInterpreter from './LatencyInterpreter'; import LatencyWatcher from './LatencyWatcher'; @@ -72,7 +73,9 @@ ipc.config.id = 'txmanager'; ipc.config.silent = true; ipc.serve('/tmp/newbedford.txmanager', () => { ipc.server.on('liquidation-candidate-add', (message) => { - console.log(message); + const candidate = message as ILiquidationCandidate; + if (candidate.expectedRevenue < 0.1) return; + txmanager.addLiquidationCandidate(message as ILiquidationCandidate) }); ipc.server.on('liquidation-candidate-remove', (message) => { console.log(message); @@ -82,6 +85,7 @@ ipc.server.start(); process.on('SIGINT', () => { console.log('\nCaught interrupt signal'); + ipc.server.stop(); txmanager.stop(); provider.eth.clearSubscriptions(); diff --git a/services/txmanager/src/types/ILiquidationCandidate.ts b/services/txmanager/src/types/ILiquidationCandidate.ts index 142f7a8..ce871b3 100644 --- a/services/txmanager/src/types/ILiquidationCandidate.ts +++ b/services/txmanager/src/types/ILiquidationCandidate.ts @@ -1,10 +1,10 @@ -import IOpenOraclePriceData from './IOpenOraclePriceData'; +import IPostablePriceFormat from './IPostablePriceFormat'; import { CTokens } from './CTokens'; export default interface ILiquidationCandidate { address: string; repayCToken: CTokens; seizeCToken: CTokens; - pricesToReport: IOpenOraclePriceData; + pricesToReport: IPostablePriceFormat; expectedRevenue: number; } diff --git a/services/txmanager/src/types/IOpenOraclePriceData.ts b/services/txmanager/src/types/IPostablePriceFormat.ts similarity index 58% rename from services/txmanager/src/types/IOpenOraclePriceData.ts rename to services/txmanager/src/types/IPostablePriceFormat.ts index 45c90c1..41c26c7 100644 --- a/services/txmanager/src/types/IOpenOraclePriceData.ts +++ b/services/txmanager/src/types/IPostablePriceFormat.ts @@ -1,4 +1,4 @@ -export default interface IOpenOraclePriceData { +export default interface IPostablePriceFormat { messages: string[]; signatures: string[]; symbols: string[];