From 931722088df2b13697b5e269636ef0061c9e85c6 Mon Sep 17 00:00:00 2001 From: Corey Rice Date: Fri, 6 Sep 2024 11:35:32 -0300 Subject: [PATCH] feat: handle liquidation seize events --- .../isolated-pools/src/mappings/vToken.ts | 92 +++++++++++++------ .../src/operations/getOrCreate.ts | 20 +++- .../operations/recordLiquidatorAsBorrower.ts | 60 ++++++++++++ .../isolated-pools/src/operations/update.ts | 4 +- .../src/operations/updateOrCreate.ts | 3 +- subgraphs/isolated-pools/template.yaml | 2 + 6 files changed, 147 insertions(+), 34 deletions(-) create mode 100644 subgraphs/isolated-pools/src/operations/recordLiquidatorAsBorrower.ts diff --git a/subgraphs/isolated-pools/src/mappings/vToken.ts b/subgraphs/isolated-pools/src/mappings/vToken.ts index 8e4f6f87..b496aa24 100644 --- a/subgraphs/isolated-pools/src/mappings/vToken.ts +++ b/subgraphs/isolated-pools/src/mappings/vToken.ts @@ -9,12 +9,14 @@ import { NewAccessControlManager, NewMarketInterestRateModel, NewReserveFactor, + ProtocolSeize, Redeem, RepayBorrow, ReservesAdded, SpreadReservesReduced, Transfer, } from '../../generated/PoolRegistry/VToken'; +import { VToken as VTokenContract } from '../../generated/PoolRegistry/VToken'; import { nullAddress } from '../constants/addresses'; import { oneBigInt, zeroBigInt32 } from '../constants/index'; import { @@ -26,13 +28,17 @@ import { createRepayBorrowTransaction, createTransferTransaction, } from '../operations/create'; -import { getOrCreateAccount, getOrCreateMarket } from '../operations/getOrCreate'; +import { getMarket } from '../operations/get'; +import { + getOrCreateAccount, + getOrCreateAccountVToken, + getOrCreateMarket, +} from '../operations/getOrCreate'; +import { recordLiquidatorAsBorrower } from '../operations/recordLiquidatorAsBorrower'; import { updateAccountVTokenBorrow, updateAccountVTokenRepayBorrow, updateAccountVTokenSupply, - updateAccountVTokenTransferFrom, - updateAccountVTokenTransferTo, updateMarket, } from '../operations/update'; @@ -190,6 +196,28 @@ export function handleLiquidateBorrow(event: LiquidateBorrow): void { const borrower = getOrCreateAccount(event.params.borrower); borrower.countLiquidated = borrower.countLiquidated + 1; borrower.save(); + const collateralContract = VTokenContract.bind(event.params.vTokenCollateral); + const collateralBalance = collateralContract.balanceOf(event.params.borrower); + const market = getMarket(event.address)!; + // Update balance + const borrowerAccountVToken = getOrCreateAccountVToken( + event.params.borrower, + Address.fromBytes(market.pool), + event.address, + ); + const accountFromVToken = borrowerAccountVToken.entity; + + // Creation updates balance + if (!borrowerAccountVToken.created) { + accountFromVToken.accountVTokenSupplyBalanceMantissa = + accountFromVToken.accountVTokenSupplyBalanceMantissa.minus(event.params.seizeTokens); + } + accountFromVToken.save(); + // Check if borrower is still supplying liquidated asset + if (collateralBalance.equals(zeroBigInt32)) { + market.supplierCount = market.supplierCount.plus(oneBigInt); + market.save(); + } createLiquidateBorrowTransaction(event); } @@ -205,6 +233,10 @@ export function handleNewReserveFactor(event: NewReserveFactor): void { market.save(); } +export function handleProtocolSeize(event: ProtocolSeize): void { + recordLiquidatorAsBorrower(event); +} + /* Transferring of vTokens * * event.params.from = sender of vTokens @@ -221,44 +253,50 @@ export function handleNewReserveFactor(event: NewReserveFactor): void { * events. So for those events, we do not update vToken balances. */ export function handleTransfer(event: Transfer): void { - const vTokenAddress = event.address; - const accountFromAddress = event.params.from; - const accountToAddress = event.params.to; - + // We only updateMarket() if accrual block number is not up to date. This will only happen + // with normal transfers, since mint, redeem, and seize transfers will already run updateMarket() const market = updateMarket( event.address, event.block.number.toI32(), event.block.timestamp.toI32(), ); - - // Checking if the tx is FROM the vToken contract (i.e. this will not run when minting) - // If so, it is a mint, and we don't need to run these calculations - if (accountFromAddress.toHex() != nullAddress.toHex()) { + const accountFromAddress = event.params.from; + const accountToAddress = event.params.to; + // Checking if the event is FROM the vToken contract or null (i.e. this will not run when minting) + // Checking if the event is TO the vToken contract (i.e. this will not run when redeeming) + // @TODO Edge case where someone who accidentally sends vTokens to a vToken contract, where it will not get recorded. + if ( + accountFromAddress.notEqual(event.address) && + accountFromAddress.notEqual(nullAddress) && + accountToAddress.notEqual(event.address) + ) { getOrCreateAccount(accountFromAddress); - - updateAccountVTokenTransferFrom( + const resultFrom = getOrCreateAccountVToken( accountFromAddress, Address.fromBytes(market.pool), - vTokenAddress, - event.block.number, - event.params.amount, - market.exchangeRateMantissa, + event.address, ); - } + const accountFromVToken = resultFrom.entity; - // Checking if the tx is TO the vToken contract (i.e. this will not run when redeeming) - // If so, we ignore it. this leaves an edge case, where someone who accidentally sends - // vTokens to a vToken contract, where it will not get recorded. Right now it would - // be messy to include, so we are leaving it out for now TODO fix this in future - if (accountToAddress.toHex() != vTokenAddress.toHex()) { + // Creation updates balance + if (!resultFrom.created) { + accountFromVToken.accountVTokenSupplyBalanceMantissa = + accountFromVToken.accountVTokenSupplyBalanceMantissa.minus(event.params.amount); + } + accountFromVToken.save(); getOrCreateAccount(accountToAddress); - - updateAccountVTokenTransferTo( + const resultTo = getOrCreateAccountVToken( accountToAddress, Address.fromBytes(market.pool), - vTokenAddress, - event.block.number, + event.address, ); + const accountToVToken = resultTo.entity; + // Creation updates balance + if (!resultTo.created) { + accountToVToken.accountVTokenSupplyBalanceMantissa = + accountToVToken.accountVTokenSupplyBalanceMantissa.plus(event.params.amount); + accountToVToken.save(); + } } createTransferTransaction(event); diff --git a/subgraphs/isolated-pools/src/operations/getOrCreate.ts b/subgraphs/isolated-pools/src/operations/getOrCreate.ts index b216ba08..2bc7b695 100644 --- a/subgraphs/isolated-pools/src/operations/getOrCreate.ts +++ b/subgraphs/isolated-pools/src/operations/getOrCreate.ts @@ -68,36 +68,48 @@ export const getOrCreateAccountPool = ( return accountPool; }; +export class GetOrCreateAccountVTokenReturn { + entity: AccountVToken; + created: boolean; +} + export const getOrCreateAccountVToken = ( accountAddress: Address, poolAddress: Address, marketAddress: Address, enteredMarket: boolean = false, // eslint-disable-line @typescript-eslint/no-inferrable-types -): AccountVToken => { +): GetOrCreateAccountVTokenReturn => { const accountVTokenId = getAccountVTokenId(marketAddress, accountAddress); let accountVToken = AccountVToken.load(accountVTokenId); + let created = false; if (!accountVToken) { + created = true; accountVToken = new AccountVToken(accountVTokenId); accountVToken.account = accountAddress; accountVToken.accountPool = getOrCreateAccountPool(accountAddress, poolAddress).id; accountVToken.market = marketAddress; accountVToken.enteredMarket = enteredMarket; accountVToken.accrualBlockNumber = zeroBigInt32; - // we need to set an initial real onchain value to this otherwise it will never - // be accurate const vTokenContract = VToken.bind(marketAddress); const accountSnapshot = vTokenContract.getAccountSnapshot(accountAddress); const suppliedAmountMantissa = accountSnapshot.value1; const borrowedAmountMantissa = accountSnapshot.value2; + accountVToken.accountVTokenSupplyBalanceMantissa = suppliedAmountMantissa; accountVToken.accountBorrowBalanceMantissa = borrowedAmountMantissa; + // @TODO + // accountVToken.vTokenBalanceMantissa = vTokenContract.balanceOf(accountId); + // accountVToken.storedBorrowBalanceMantissa = zeroBigInt32; + // accountVToken.borrowIndex = vTokenContract.borrowIndex(); accountVToken.totalUnderlyingRedeemedMantissa = zeroBigInt32; accountVToken.accountBorrowIndexMantissa = zeroBigInt32; accountVToken.totalUnderlyingRepaidMantissa = zeroBigInt32; + accountVToken.enteredMarket = false; + accountVToken.save(); } - return accountVToken; + return { entity: accountVToken, created }; }; export const getOrCreateRewardSpeed = ( diff --git a/subgraphs/isolated-pools/src/operations/recordLiquidatorAsBorrower.ts b/subgraphs/isolated-pools/src/operations/recordLiquidatorAsBorrower.ts new file mode 100644 index 00000000..eb9a71d7 --- /dev/null +++ b/subgraphs/isolated-pools/src/operations/recordLiquidatorAsBorrower.ts @@ -0,0 +1,60 @@ +import { Address, ByteArray, Bytes, crypto, ethereum } from '@graphprotocol/graph-ts'; + +import { VToken as VTokenContract } from '../../generated/PoolRegistry/VToken'; +import { ProtocolSeize } from '../../generated/PoolRegistry/VToken'; +import { oneBigInt } from '../constants'; +import { getMarket } from '../operations/get'; +import { getOrCreateAccountVToken } from '../operations/getOrCreate'; + +class LiquidatorTransferMarker { + current: i32; + vTokenAddress: Address; + + constructor(vTokenAddress: Address) { + this.vTokenAddress = vTokenAddress; + } +} + +export const recordLiquidatorAsBorrower = (event: ProtocolSeize): void => { + if (event.receipt) { + const reversedLogs = event.receipt!.logs.reverse(); + reversedLogs.reduce((acc, curr, idx) => { + const protocolSeizeTopic = Bytes.fromByteArray( + crypto.keccak256(ByteArray.fromUTF8('ProtocolSeize(address,address,uint256)')), + ); + + if (curr.topics.includes(protocolSeizeTopic)) { + acc.current = idx - 1; + } + if (idx == acc.current) { + const transferTuple = ethereum.decode('(address,address,uint256)', curr.data)!.toTuple(); + const liquidator = transferTuple[1].toAddress(); + const amount = transferTuple[2].toBigInt(); + // Register a Transfer to liquidator +1 supplier count + const collateralContract = VTokenContract.bind(acc.vTokenAddress); + const collateralBalanceLiquidator = collateralContract.balanceOf(liquidator); + const market = getMarket(acc.vTokenAddress)!; + // Update balance + const borrowerAccountVToken = getOrCreateAccountVToken( + liquidator, + Address.fromBytes(market.pool), + acc.vTokenAddress, + ); + const accountFromVToken = borrowerAccountVToken.entity; + + // Creation updates balance + if (!borrowerAccountVToken.created) { + accountFromVToken.accountVTokenSupplyBalanceMantissa = + accountFromVToken.accountVTokenSupplyBalanceMantissa.minus(amount); + } + accountFromVToken.save(); + // If the transfer amount equals balance it was a funding transfer + if (collateralBalanceLiquidator.equals(amount)) { + market.supplierCount = market.supplierCount.plus(oneBigInt); + market.save(); + } + } + return acc; + }, new LiquidatorTransferMarker(event.address)); + } +}; diff --git a/subgraphs/isolated-pools/src/operations/update.ts b/subgraphs/isolated-pools/src/operations/update.ts index 206ef010..cd92cfc5 100644 --- a/subgraphs/isolated-pools/src/operations/update.ts +++ b/subgraphs/isolated-pools/src/operations/update.ts @@ -17,8 +17,8 @@ const updateAccountVToken = ( ): AccountVToken => { getOrCreateAccount(accountAddress); const accountVToken = getOrCreateAccountVToken(accountAddress, poolAddress, marketAddress, false); - accountVToken.accrualBlockNumber = blockNumber; - return accountVToken as AccountVToken; + accountVToken.entity.accrualBlockNumber = blockNumber; + return accountVToken.entity as AccountVToken; }; export const updateAccountVTokenSupply = ( diff --git a/subgraphs/isolated-pools/src/operations/updateOrCreate.ts b/subgraphs/isolated-pools/src/operations/updateOrCreate.ts index f6dce18f..a80da47b 100644 --- a/subgraphs/isolated-pools/src/operations/updateOrCreate.ts +++ b/subgraphs/isolated-pools/src/operations/updateOrCreate.ts @@ -18,12 +18,13 @@ export const updateOrCreateAccountVToken = ( enteredMarketBool = enteredMarket.value; } - const accountVToken = getOrCreateAccountVToken( + const result = getOrCreateAccountVToken( accountAddress, poolAddress, marketAddress, enteredMarketBool, ); + const accountVToken = result.entity; accountVToken.enteredMarket = enteredMarketBool; accountVToken.accrualBlockNumber = blockNumber; accountVToken.save(); diff --git a/subgraphs/isolated-pools/template.yaml b/subgraphs/isolated-pools/template.yaml index 3ff5bdc4..48844454 100644 --- a/subgraphs/isolated-pools/template.yaml +++ b/subgraphs/isolated-pools/template.yaml @@ -129,6 +129,8 @@ templates: handler: handleRepayBorrow - event: LiquidateBorrow(indexed address,indexed address,uint256,indexed address,uint256) handler: handleLiquidateBorrow + - event: ProtocolSeize(indexed address,indexed address,uint256) + handler: handleProtocolSeize - event: AccrueInterest(uint256,uint256,uint256,uint256) handler: handleAccrueInterest - event: NewReserveFactor(uint256,uint256)