diff --git a/package.json b/package.json index 72c6c679..728b3bb1 100644 --- a/package.json +++ b/package.json @@ -33,8 +33,8 @@ }, "devDependencies": { "@graphprotocol/client-cli": "^3.0.0", - "@graphprotocol/graph-cli": "0.56.0", - "@graphprotocol/graph-ts": "0.31.0", + "@graphprotocol/graph-cli": "^0.67.1", + "@graphprotocol/graph-ts": "^0.32.0", "@nomicfoundation/hardhat-chai-matchers": "^1.0.3", "@nomicfoundation/hardhat-network-helpers": "^1.0.4", "@nomicfoundation/hardhat-toolbox": "^1.0.2", diff --git a/subgraphs/venus/schema.graphql b/subgraphs/venus/schema.graphql index 57db8d6a..e5eba4ec 100644 --- a/subgraphs/venus/schema.graphql +++ b/subgraphs/venus/schema.graphql @@ -134,7 +134,7 @@ type AccountVToken @entity { """ Auxiliary entity for AccountVToken """ -type AccountVTokenTransaction @entity { +type AccountVTokenTransaction @entity(immutable: true) { id: ID! account: AccountVToken! @@ -171,7 +171,7 @@ interface VTokenTransfer { TransferEvent will be stored for every mint, redeem, liquidation, and any normal transfer between two accounts. """ -type TransferEvent implements VTokenTransfer @entity { +type TransferEvent implements VTokenTransfer @entity(immutable: true) { "Transaction hash concatenated with log index" id: ID! "vTokens transferred" @@ -192,7 +192,7 @@ type TransferEvent implements VTokenTransfer @entity { MintEvent stores information for mints. From address will always be a vToken market address """ -type MintEvent implements VTokenTransfer @entity { +type MintEvent implements VTokenTransfer @entity(immutable: true) { "Transaction hash concatenated with log index" id: ID! "vTokens transferred" @@ -215,7 +215,7 @@ type MintEvent implements VTokenTransfer @entity { RedeemEvent stores information for redeems. To address will always be a vToken market address """ -type RedeemEvent implements VTokenTransfer @entity { +type RedeemEvent implements VTokenTransfer @entity(immutable: true) { "Transaction hash concatenated with log index" id: ID! "vTokens transferred" @@ -238,7 +238,7 @@ type RedeemEvent implements VTokenTransfer @entity { LiquidationEvent stores information for liquidations. The event is emitted from the vToken market address. """ -type LiquidationEvent implements VTokenTransfer @entity { +type LiquidationEvent implements VTokenTransfer @entity(immutable: true) { "Transaction hash concatenated with log index" id: ID! "vTokens seized" @@ -283,7 +283,7 @@ interface UnderlyingTransfer { """ BorrowEvent stores information for borrows """ -type BorrowEvent implements UnderlyingTransfer @entity { +type BorrowEvent implements UnderlyingTransfer @entity(immutable: true) { "Transaction hash concatenated with log index" id: ID! "Amount of underlying borrowed" @@ -304,7 +304,7 @@ type BorrowEvent implements UnderlyingTransfer @entity { RepayEvent stores information for repays. Payer is not always the same as borrower, such as in the event of a Liquidation """ -type RepayEvent implements UnderlyingTransfer @entity { +type RepayEvent implements UnderlyingTransfer @entity(immutable: true) { "Transaction hash concatenated with log index" id: ID! "Amount of underlying repaid" diff --git a/subgraphs/venus/src/mappings/comptroller.ts b/subgraphs/venus/src/mappings/comptroller.ts index 23274992..a65d91e8 100644 --- a/subgraphs/venus/src/mappings/comptroller.ts +++ b/subgraphs/venus/src/mappings/comptroller.ts @@ -1,7 +1,5 @@ /* eslint-disable prefer-const */ // to satisfy AS compiler -import { log } from '@graphprotocol/graph-ts'; - import { DistributedSupplierVenus, MarketEntered, @@ -12,86 +10,36 @@ import { NewLiquidationIncentive, NewPriceOracle, } from '../../generated/Comptroller/Comptroller'; -import { Account, Market } from '../../generated/schema'; -import { VToken, VTokenUpdatedEvents } from '../../generated/templates'; -import { createAccount, createMarket } from '../operations/create'; -import { getOrCreateComptroller } from '../operations/getOrCreate'; -import { updateCommonVTokenStats } from '../operations/update'; +import { + getOrCreateAccount, + getOrCreateAccountVToken, + getOrCreateAccountVTokenTransaction, + getOrCreateComptroller, + getOrCreateMarket, +} from '../operations/getOrCreate'; export function handleMarketListed(event: MarketListed): void { - // Dynamically index all new listed tokens - VToken.create(event.params.vToken); - VTokenUpdatedEvents.create(event.params.vToken); - // Create the market for this token, since it's now been listed. - createMarket(event.params.vToken.toHexString()); + getOrCreateMarket(event.params.vToken, event); } export function handleMarketEntered(event: MarketEntered): void { - let market = Market.load(event.params.vToken.toHexString()); - // Null check needed to avoid crashing on a new market added. Ideally when dynamic data - // sources can source from the contract creation block and not the time the - // comptroller adds the market, we can avoid this altogether - if (!market) { - log.debug('[handleMarketEntered] market null: {}', [event.params.vToken.toHexString()]); - market = Market.load(event.params.vToken.toHexString()); - } - - if (!market) { - log.debug('[handleMarketEntered] market still null, return...', []); - return; - } + const market = getOrCreateMarket(event.params.vToken, event); + const account = getOrCreateAccount(event.params.account.toHex()); + const accountVToken = getOrCreateAccountVToken(market.id, market.symbol, account.id, event); + accountVToken.enteredMarket = true; + accountVToken.save(); - let accountId = event.params.account.toHex(); - let account = Account.load(accountId); - if (account == null) { - createAccount(accountId); - } - - let vTokenStats = updateCommonVTokenStats( - market.id, - market.symbol, - accountId, - event.transaction.hash, - event.block.timestamp, - event.block.number, - event.logIndex, - ); - vTokenStats.enteredMarket = true; - vTokenStats.save(); + getOrCreateAccountVTokenTransaction(accountVToken.id, event); } export function handleMarketExited(event: MarketExited): void { - let market = Market.load(event.params.vToken.toHexString()); - // Null check needed to avoid crashing on a new market added. Ideally when dynamic data - // sources can source from the contract creation block and not the time the - // comptroller adds the market, we can avoid this altogether - if (!market) { - log.debug('[handleMarketExited] market null: {}', [event.params.vToken.toHexString()]); - market = Market.load(event.params.vToken.toHexString()); - } + const market = getOrCreateMarket(event.params.vToken, event); + const account = getOrCreateAccount(event.params.account.toHex()); + const accountVToken = getOrCreateAccountVToken(market.id, market.symbol, account.id, event); + accountVToken.enteredMarket = false; + accountVToken.save(); - if (!market) { - log.debug('[handleMarketExited] market still null, return...', []); - return; - } - - let accountID = event.params.account.toHex(); - let account = Account.load(accountID); - if (account == null) { - createAccount(accountID); - } - - let vTokenStats = updateCommonVTokenStats( - market.id, - market.symbol, - accountID, - event.transaction.hash, - event.block.timestamp, - event.block.number, - event.logIndex, - ); - vTokenStats.enteredMarket = false; - vTokenStats.save(); + getOrCreateAccountVTokenTransaction(accountVToken.id, event); } export function handleNewCloseFactor(event: NewCloseFactor): void { @@ -101,14 +49,9 @@ export function handleNewCloseFactor(event: NewCloseFactor): void { } export function handleNewCollateralFactor(event: NewCollateralFactor): void { - let market = Market.load(event.params.vToken.toHexString()); - // Null check needed to avoid crashing on a new market added. Ideally when dynamic data - // sources can source from the contract creation block and not the time the - // comptroller adds the market, we can avoid this altogether - if (market != null) { - market.collateralFactorMantissa = event.params.newCollateralFactorMantissa; - market.save(); - } + const market = getOrCreateMarket(event.params.vToken, event); + market.collateralFactorMantissa = event.params.newCollateralFactorMantissa; + market.save(); } // This should be the first event acccording to bscscan but it isn't.... price oracle is. weird @@ -126,11 +69,9 @@ export function handleNewPriceOracle(event: NewPriceOracle): void { // Also handles DistributedBorrowerVenus with same signature export function handleXvsDistributed(event: DistributedSupplierVenus): void { - let vTokenAddress = event.params.vToken.toHex(); - const venusDelta = event.params.venusDelta; - let market = Market.load(vTokenAddress); - if (market == null) { - market = createMarket(vTokenAddress); - } - market.totalXvsDistributedMantissa = market.totalXvsDistributedMantissa.plus(venusDelta); + const market = getOrCreateMarket(event.params.vToken, event); + market.totalXvsDistributedMantissa = market.totalXvsDistributedMantissa.plus( + event.params.venusDelta, + ); + market.save(); } diff --git a/subgraphs/venus/src/mappings/vToken.ts b/subgraphs/venus/src/mappings/vToken.ts index a0f6ea6e..522db955 100644 --- a/subgraphs/venus/src/mappings/vToken.ts +++ b/subgraphs/venus/src/mappings/vToken.ts @@ -1,13 +1,7 @@ /* eslint-disable prefer-const */ // to satisfy AS compiler -import { - Account, - BorrowEvent, - LiquidationEvent, - Market, - RepayEvent, - TransferEvent, -} from '../../generated/schema'; +import { Address, BigInt } from '@graphprotocol/graph-ts'; + import { AccrueInterest, Borrow, @@ -27,18 +21,22 @@ import { Redeem, } from '../../generated/templates/VTokenUpdatedEvents/VTokenUpdatedEvents'; import { DUST_THRESHOLD, oneBigInt, zeroBigInt32 } from '../constants'; -import { nullAddress } from '../constants/addresses'; import { - createAccount, - createMarket, + createBorrowEvent, + createLiquidationEvent, createMintBehalfEvent, createMintEvent, createRedeemEvent, + createRepayEvent, + createTransferEvent, } from '../operations/create'; -import { updateCommonVTokenStats } from '../operations/update'; -import { updateMarket } from '../operations/update'; +import { + getOrCreateAccount, + getOrCreateAccountVToken, + getOrCreateAccountVTokenTransaction, + getOrCreateMarket, +} from '../operations/getOrCreate'; import { exponentToBigInt } from '../utilities/exponentToBigInt'; -import { getMarketId, getTransactionId } from '../utilities/ids'; /* Account supplies assets into market and receives vTokens in exchange * @@ -49,36 +47,25 @@ import { getMarketId, getTransactionId } from '../utilities/ids'; * Notes * Transfer event will always get emitted with this * Mints originate from the vToken address, not 0x000000, which is typical of ERC-20s - * No need to updateMarket(), handleAccrueInterest() ALWAYS runs before this - * No need to updateCommonVTokenStats, handleTransfer() will + * No need to update AccountVToken, handleTransfer() will * No need to update vTokenBalance, handleTransfer() will */ export function handleMint(event: Mint): void { - let market = Market.load(event.address.toHexString()); - if (!market) { - market = createMarket(event.address.toHexString()); - } - - createMintEvent(event); - + const market = getOrCreateMarket(event.address, event); if (event.params.mintTokens.equals(event.params.totalSupply)) { market.supplierCount = market.supplierCount.plus(oneBigInt); market.save(); } + createMintEvent(event); } export function handleMintBehalf(event: MintBehalf): void { - let market = Market.load(event.address.toHexString()); - if (!market) { - market = createMarket(event.address.toHexString()); - } - - createMintBehalfEvent(event); - + const market = getOrCreateMarket(event.address, event); if (event.params.mintTokens.equals(event.params.totalSupply)) { market.supplierCount = market.supplierCount.plus(oneBigInt); market.save(); } + createMintBehalfEvent(event); } /* Account supplies vTokens into market and receives underlying asset in exchange @@ -89,23 +76,17 @@ export function handleMintBehalf(event: MintBehalf): void { * * Notes * Transfer event will always get emitted with this - * No need to updateMarket(), handleAccrueInterest() ALWAYS runs before this - * No need to updateCommonVTokenStats, handleTransfer() will + * No need to update AccountVToken, handleTransfer() will * No need to update vTokenBalance, handleTransfer() will */ export function handleRedeem(event: Redeem): void { - let market = Market.load(event.address.toHexString()); - if (!market) { - market = createMarket(event.address.toHexString()); - } - - createRedeemEvent(event); - + const market = getOrCreateMarket(event.address, event); if (event.params.totalSupply.equals(zeroBigInt32)) { // if the current balance is 0 then the user has withdrawn all their assets from this market market.supplierCount = market.supplierCount.minus(oneBigInt); market.save(); } + createRedeemEvent(event); } /* Borrow assets from the protocol. All values either BNB or BEP20 @@ -115,61 +96,29 @@ export function handleRedeem(event: Redeem): void { * event.params.borrowAmount = that was added in this event * event.params.borrower = the account * Notes - * No need to updateMarket(), handleAccrueInterest() ALWAYS runs before this */ export function handleBorrow(event: Borrow): void { - let market = Market.load(event.address.toHexString()); - if (!market) { - market = createMarket(event.address.toHexString()); - } - let accountID = event.params.borrower.toHex(); - let account = Account.load(accountID); - if (account == null) { - account = createAccount(accountID); - } - account.hasBorrowed = true; - account.save(); - - // Update vTokenStats common for all events, and return the stats to update unique - // values for each event - let vTokenStats = updateCommonVTokenStats( - market.id, - market.symbol, - accountID, - event.transaction.hash, - event.block.timestamp, - event.block.number, - event.logIndex, - ); - - vTokenStats.storedBorrowBalanceMantissa = event.params.accountBorrows; - - vTokenStats.accountBorrowIndexMantissa = market.borrowIndexMantissa; - vTokenStats.totalUnderlyingBorrowedMantissa = vTokenStats.totalUnderlyingBorrowedMantissa.plus( - event.params.borrowAmount, - ); - vTokenStats.save(); - - let borrowID = event.transaction.hash - .toHexString() - .concat('-') - .concat(event.transactionLogIndex.toString()); - - let borrow = new BorrowEvent(borrowID); - borrow.amountMantissa = event.params.borrowAmount; - borrow.accountBorrowsMantissa = event.params.accountBorrows; - borrow.borrower = event.params.borrower; - borrow.blockNumber = event.block.number.toI32(); - borrow.blockTime = event.block.timestamp.toI32(); - borrow.underlyingAddress = market.underlyingAddress; - borrow.save(); - + const market = getOrCreateMarket(event.address, event); if (event.params.accountBorrows == event.params.borrowAmount) { // if both the accountBorrows and the borrowAmount are the same, it means the account is a new borrower market.borrowerCount = market.borrowerCount.plus(oneBigInt); market.borrowerCountAdjusted = market.borrowerCountAdjusted.plus(oneBigInt); market.save(); } + + const account = getOrCreateAccount(event.params.borrower.toHex()); + account.hasBorrowed = true; + account.save(); + + const accountVToken = getOrCreateAccountVToken(market.id, market.symbol, account.id, event); + accountVToken.storedBorrowBalanceMantissa = event.params.accountBorrows; + accountVToken.accountBorrowIndexMantissa = market.borrowIndexMantissa; + accountVToken.totalUnderlyingBorrowedMantissa = + accountVToken.totalUnderlyingBorrowedMantissa.plus(event.params.borrowAmount); + accountVToken.save(); + + getOrCreateAccountVTokenTransaction(accountVToken.id, event); + createBorrowEvent(event, market.underlyingAddress); } /* Repay some amount borrowed. Anyone can repay anyones balance @@ -181,57 +130,12 @@ export function handleBorrow(event: Borrow): void { * event.params.payer = the payer * * Notes - * No need to updateMarket(), handleAccrueInterest() ALWAYS runs before this * Once a account totally repays a borrow, it still has its account interest index set to the * markets value. We keep this, even though you might think it would reset to 0 upon full * repay. */ export function handleRepayBorrow(event: RepayBorrow): void { - let market = Market.load(event.address.toHexString()); - if (!market) { - market = createMarket(event.address.toHexString()); - } - let accountID = event.params.borrower.toHex(); - let account = Account.load(accountID); - if (account == null) { - createAccount(accountID); - } - - // Update vTokenStats common for all events, and return the stats to update unique - // values for each event - let vTokenStats = updateCommonVTokenStats( - market.id, - market.symbol, - accountID, - event.transaction.hash, - event.block.timestamp, - event.block.number, - event.logIndex, - ); - - vTokenStats.storedBorrowBalanceMantissa = event.params.accountBorrows; - - vTokenStats.accountBorrowIndexMantissa = market.borrowIndexMantissa; - vTokenStats.totalUnderlyingRepaidMantissa = vTokenStats.totalUnderlyingRepaidMantissa.plus( - event.params.repayAmount, - ); - vTokenStats.save(); - - let repayID = event.transaction.hash - .toHexString() - .concat('-') - .concat(event.transactionLogIndex.toString()); - - let repay = new RepayEvent(repayID); - repay.amountMantissa = event.params.repayAmount; - repay.accountBorrowsMantissa = event.params.accountBorrows; - repay.borrower = event.params.borrower; - repay.blockNumber = event.block.number.toI32(); - repay.blockTime = event.block.timestamp.toI32(); - repay.underlyingAddress = market.underlyingAddress; - repay.payer = event.params.payer; - repay.save(); - + const market = getOrCreateMarket(event.address, event); if (event.params.accountBorrows.equals(zeroBigInt32)) { market.borrowerCount = market.borrowerCount.minus(oneBigInt); market.borrowerCountAdjusted = market.borrowerCountAdjusted.minus(oneBigInt); @@ -242,6 +146,19 @@ export function handleRepayBorrow(event: RepayBorrow): void { market.borrowerCountAdjusted = market.borrowerCountAdjusted.minus(oneBigInt); market.save(); } + + const account = getOrCreateAccount(event.params.borrower.toHex()); + + const accountVToken = getOrCreateAccountVToken(market.id, market.symbol, account.id, event); + accountVToken.storedBorrowBalanceMantissa = event.params.accountBorrows; + accountVToken.accountBorrowIndexMantissa = market.borrowIndexMantissa; + accountVToken.totalUnderlyingRepaidMantissa = accountVToken.totalUnderlyingRepaidMantissa.plus( + event.params.repayAmount, + ); + accountVToken.save(); + + getOrCreateAccountVTokenTransaction(accountVToken.id, event); + createRepayEvent(event, market.underlyingAddress); } /* @@ -254,26 +171,19 @@ export function handleRepayBorrow(event: RepayBorrow): void { * event.params.seizeTokens - vTokens seized (transfer event should handle this) * * Notes - * No need to updateMarket(), handleAccrueInterest() ALWAYS runs before this. * When calling this const, event RepayBorrow, and event Transfer will be called every * time. This means we can ignore repayAmount. Seize tokens only changes state * of the vTokens, which is covered by transfer. Therefore we only * add liquidation counts in this handler. */ export function handleLiquidateBorrow(event: LiquidateBorrow): void { - let liquidatorID = event.params.liquidator.toHex(); - let liquidator = Account.load(liquidatorID); - if (liquidator == null) { - liquidator = createAccount(liquidatorID); - } + const market = getOrCreateMarket(event.address, event); + + const liquidator = getOrCreateAccount(event.params.liquidator.toHex()); liquidator.countLiquidator = liquidator.countLiquidator + 1; liquidator.save(); - let borrowerID = event.params.borrower.toHex(); - let borrower = Account.load(borrowerID); - if (borrower == null) { - borrower = createAccount(borrowerID); - } + const borrower = getOrCreateAccount(event.params.borrower.toHex()); borrower.countLiquidated = borrower.countLiquidated + 1; borrower.save(); @@ -281,25 +191,7 @@ export function handleLiquidateBorrow(event: LiquidateBorrow): void { // asset. They seize one of potentially many types of vToken collateral of // the underwater borrower. So we must get that address from the event, and // the repay token is the event.address - let marketRepayToken = Market.load(event.address.toHexString()); - if (!marketRepayToken) { - marketRepayToken = createMarket(event.address.toHexString()); - } - let mintID = event.transaction.hash - .toHexString() - .concat('-') - .concat(event.transactionLogIndex.toString()); - - let liquidation = new LiquidationEvent(mintID); - liquidation.amountMantissa = event.params.seizeTokens; - liquidation.to = event.params.liquidator; - liquidation.from = event.params.borrower; - liquidation.blockNumber = event.block.number.toI32(); - liquidation.blockTime = event.block.timestamp.toI32(); - liquidation.underlyingRepaidAddress = marketRepayToken.underlyingAddress; - liquidation.underlyingRepayAmountMantissa = event.params.repayAmount; - liquidation.vTokenCollateralAddress = event.params.vTokenCollateral; - liquidation.save(); + createLiquidationEvent(event, market.underlyingAddress); } /* Transferring of vTokens @@ -320,164 +212,99 @@ export function handleLiquidateBorrow(event: LiquidateBorrow): void { export function handleTransfer(event: Transfer): void { // 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() - let marketId = getMarketId(event.address); - let market = Market.load(marketId); - if (!market) { - market = createMarket(marketId); - } - if (market.accrualBlockNumber != event.block.number.toI32()) { - market = updateMarket(event.address, event.block.number.toI32(), event.block.timestamp.toI32()); - } + let market = getOrCreateMarket(event.address, event); - let amountUnderlying = market.exchangeRateMantissa + const amountUnderlying = market.exchangeRateMantissa .times(event.params.amount) .div(exponentToBigInt(18)); // 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 - let accountFromId = event.params.from.toHex(); - if (accountFromId != nullAddress.toHex()) { - let accountFrom = Account.load(accountFromId); - if (accountFrom == null) { - createAccount(accountFromId); - } - - // Update vTokenStats common for all events, and return the stats to update unique - // values for each event - let vTokenStatsFrom = updateCommonVTokenStats( + let accountFromAddress = event.params.from; + if (accountFromAddress != Address.fromString(market.id)) { + const accountFrom = getOrCreateAccount(accountFromAddress.toHex()); + const accountFromVToken = getOrCreateAccountVToken( market.id, market.symbol, - accountFromId, - event.transaction.hash, - event.block.timestamp, - event.block.number, - event.logIndex, + accountFrom.id, + event, ); - - vTokenStatsFrom.vTokenBalanceMantissa = vTokenStatsFrom.vTokenBalanceMantissa.minus( + accountFromVToken.vTokenBalanceMantissa = accountFromVToken.vTokenBalanceMantissa.minus( event.params.amount, ); + accountFromVToken.totalUnderlyingRedeemedMantissa = + accountFromVToken.totalUnderlyingRedeemedMantissa.plus(amountUnderlying); + accountFromVToken.save(); - vTokenStatsFrom.totalUnderlyingRedeemedMantissa = - vTokenStatsFrom.totalUnderlyingRedeemedMantissa.plus(amountUnderlying); - vTokenStatsFrom.save(); + getOrCreateAccountVTokenTransaction(accountFromVToken.id, event); } // 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 - let accountToId = event.params.to.toHex(); - if (accountToId != marketId) { - let accountTo = Account.load(accountToId); - if (accountTo == null) { - createAccount(accountToId); - } - - // Update vTokenStats common for all events, and return the stats to update unique - // values for each event - let vTokenStatsTo = updateCommonVTokenStats( - market.id, - market.symbol, - accountToId, - event.transaction.hash, - event.block.timestamp, - event.block.number, - event.logIndex, - ); - - vTokenStatsTo.vTokenBalanceMantissa = vTokenStatsTo.vTokenBalanceMantissa.plus( + let accountToAddress = event.params.to; + if (accountToAddress != Address.fromString(market.id)) { + const accountTo = getOrCreateAccount(accountToAddress.toHex()); + const accountToVToken = getOrCreateAccountVToken(market.id, market.symbol, accountTo.id, event); + accountToVToken.vTokenBalanceMantissa = accountToVToken.vTokenBalanceMantissa.plus( event.params.amount, ); + accountToVToken.totalUnderlyingSuppliedMantissa = + accountToVToken.totalUnderlyingSuppliedMantissa.plus(amountUnderlying); + accountToVToken.save(); - vTokenStatsTo.totalUnderlyingSuppliedMantissa = - vTokenStatsTo.totalUnderlyingSuppliedMantissa.plus(amountUnderlying); - vTokenStatsTo.save(); + getOrCreateAccountVTokenTransaction(accountToVToken.id, event); } - - let transferId = getTransactionId(event.transaction.hash, event.transactionLogIndex); - - let transfer = new TransferEvent(transferId); - transfer.amountMantissa = event.params.amount; - transfer.to = event.params.to; - transfer.from = event.params.from; - transfer.blockNumber = event.block.number.toI32(); - transfer.blockTime = event.block.timestamp.toI32(); - transfer.vTokenAddress = event.address; - transfer.save(); + createTransferEvent(event); } export function handleAccrueInterest(event: AccrueInterest): void { - updateMarket(event.address, event.block.number.toI32(), event.block.timestamp.toI32()); + // updates market.accrualBlockNumber and rates + getOrCreateMarket(event.address, event); } export function handleNewReserveFactor(event: NewReserveFactor): void { - let marketID = event.address.toHex(); - let market = Market.load(marketID); - if (!market) { - market = createMarket(marketID); - } + const market = getOrCreateMarket(event.address, event); market.reserveFactor = event.params.newReserveFactorMantissa; market.save(); } export function handleNewMarketInterestRateModel(event: NewMarketInterestRateModel): void { - let marketID = event.address.toHex(); - let market = Market.load(marketID); - if (market == null) { - market = createMarket(marketID); - } + const market = getOrCreateMarket(event.address, event); market.interestRateModelAddress = event.params.newInterestRateModel; market.save(); } -export function handleMintV1(event: MintV1): void { - let market = Market.load(event.address.toHexString()); - if (!market) { - market = createMarket(event.address.toHexString()); - } - - createMintEvent(event); - - const vTokenContract = VTokenContract.bind(event.address); - let totalSupply = vTokenContract.balanceOf(event.params.minter); +function getVTokenBalance(vTokenAddress: Address, accountAddress: Address): BigInt { + const vTokenContract = VTokenContract.bind(vTokenAddress); + return vTokenContract.balanceOf(accountAddress); +} - if (event.params.mintTokens.equals(totalSupply)) { +export function handleMintV1(event: MintV1): void { + const market = getOrCreateMarket(event.address, event); + if (event.params.mintTokens.equals(getVTokenBalance(event.address, event.params.minter))) { market.supplierCount = market.supplierCount.plus(oneBigInt); market.save(); } + createMintEvent(event); } export function handleMintBehalfV1(event: MintBehalfV1): void { - let market = Market.load(event.address.toHexString()); - if (!market) { - market = createMarket(event.address.toHexString()); - } - - createMintBehalfEvent(event); - - const vTokenContract = VTokenContract.bind(event.address); - let totalSupply = vTokenContract.balanceOf(event.params.receiver); - - if (event.params.mintTokens.equals(totalSupply)) { + const market = getOrCreateMarket(event.address, event); + if (event.params.mintTokens.equals(getVTokenBalance(event.address, event.params.receiver))) { market.supplierCount = market.supplierCount.plus(oneBigInt); market.save(); } + createMintBehalfEvent(event); } export function handleRedeemV1(event: RedeemV1): void { - let market = Market.load(event.address.toHexString()); - if (!market) { - market = createMarket(event.address.toHexString()); - } - createRedeemEvent(event); - - const vTokenContract = VTokenContract.bind(event.address); - let totalSupply = vTokenContract.balanceOf(event.params.redeemer); - - if (totalSupply.equals(zeroBigInt32)) { + const market = getOrCreateMarket(event.address, event); + if (getVTokenBalance(event.address, event.params.redeemer).equals(zeroBigInt32)) { // if the current balance is 0 then the user has withdrawn all their assets from this market market.supplierCount = market.supplierCount.minus(oneBigInt); market.save(); } + createRedeemEvent(event); } diff --git a/subgraphs/venus/src/operations/create.ts b/subgraphs/venus/src/operations/create.ts index b25deb3d..cbccb728 100644 --- a/subgraphs/venus/src/operations/create.ts +++ b/subgraphs/venus/src/operations/create.ts @@ -1,114 +1,17 @@ -import { Address, BigInt, log } from '@graphprotocol/graph-ts'; - -import { Account, AccountVToken, Market, MintEvent, RedeemEvent } from '../../generated/schema'; -import { BEP20 } from '../../generated/templates/VToken/BEP20'; -import { VToken } from '../../generated/templates/VToken/VToken'; -import { zeroBigInt32 } from '../constants'; -import { nullAddress } from '../constants/addresses'; -import { getUnderlyingPrice } from '../utilities/getUnderlyingPrice'; +import { Bytes } from '@graphprotocol/graph-ts'; + +import { + BorrowEvent, + LiquidationEvent, + MintEvent, + RedeemEvent, + RepayEvent, + TransferEvent, +} from '../../generated/schema'; import { getTransactionId } from '../utilities/ids'; -export function createAccountVToken( - accountVTokenId: string, - symbol: string, - account: string, - marketId: string, -): AccountVToken { - const accountVToken = new AccountVToken(accountVTokenId); - accountVToken.symbol = symbol; - accountVToken.market = marketId; - accountVToken.account = account; - accountVToken.accrualBlockNumber = BigInt.fromI32(0); - // we need to set an initial real onchain value to this otherwise it will never be accurate - const vTokenContract = VToken.bind(Address.fromString(marketId)); - accountVToken.vTokenBalanceMantissa = vTokenContract.balanceOf(Address.fromString(account)); - - accountVToken.totalUnderlyingSuppliedMantissa = zeroBigInt32; - accountVToken.totalUnderlyingRedeemedMantissa = zeroBigInt32; - accountVToken.accountBorrowIndexMantissa = zeroBigInt32; - accountVToken.totalUnderlyingBorrowedMantissa = zeroBigInt32; - accountVToken.totalUnderlyingRepaidMantissa = zeroBigInt32; - accountVToken.storedBorrowBalanceMantissa = zeroBigInt32; - accountVToken.enteredMarket = false; - return accountVToken; -} - -export function createAccount(accountId: string): Account { - const account = new Account(accountId); - account.countLiquidated = 0; - account.countLiquidator = 0; - account.hasBorrowed = false; - account.save(); - return account; -} - -export function createMarket(marketAddress: string): Market { - let market: Market; - const vTokenContract = VToken.bind(Address.fromString(marketAddress)); - - log.debug('[createMarket] market address: {}', [marketAddress]); - - const vTokenSymbol = vTokenContract.symbol(); - - // It is vBNB, which has a slightly different interface - if (vTokenSymbol == 'vBNB') { - market = new Market(marketAddress); - market.underlyingAddress = nullAddress; - market.underlyingDecimals = 18; - market.underlyingName = 'BNB'; - market.underlyingSymbol = 'BNB'; - market.underlyingPriceCents = zeroBigInt32; - // It is all other VBEP20 contracts - } else { - market = new Market(marketAddress); - market.underlyingAddress = vTokenContract.underlying(); - log.debug('[createMarket] market underlying address: {}', [ - market.underlyingAddress.toHexString(), - ]); - const underlyingContract = BEP20.bind(Address.fromBytes(market.underlyingAddress)); - market.underlyingDecimals = underlyingContract.decimals(); - market.underlyingName = underlyingContract.name(); - market.underlyingSymbol = underlyingContract.symbol(); - - const underlyingPriceCents = getUnderlyingPrice(market.id, market.underlyingDecimals); - market.underlyingPriceCents = underlyingPriceCents; - } - - market.vTokenDecimals = vTokenContract.decimals(); - - const interestRateModelAddress = vTokenContract.try_interestRateModel(); - const reserveFactor = vTokenContract.try_reserveFactorMantissa(); - - market.borrowRateMantissa = zeroBigInt32; - market.cashMantissa = zeroBigInt32; - market.collateralFactorMantissa = zeroBigInt32; - market.exchangeRateMantissa = zeroBigInt32; - market.interestRateModelAddress = interestRateModelAddress.reverted - ? nullAddress - : interestRateModelAddress.value; - market.name = vTokenContract.name(); - market.reservesMantissa = BigInt.fromI32(0); - market.supplyRateMantissa = zeroBigInt32; - market.symbol = vTokenContract.symbol(); - market.totalBorrowsMantissa = zeroBigInt32; - market.totalSupplyMantissa = zeroBigInt32; - - market.accrualBlockNumber = 0; - market.blockTimestamp = 0; - market.borrowIndexMantissa = zeroBigInt32; - market.reserveFactor = reserveFactor.reverted ? BigInt.fromI32(0) : reserveFactor.value; - market.totalXvsDistributedMantissa = zeroBigInt32; - - market.supplierCount = zeroBigInt32; - market.borrowerCount = zeroBigInt32; - market.borrowerCountAdjusted = zeroBigInt32; - market.save(); - return market; -} - export function createMintEvent(event: E): void { const mintId = getTransactionId(event.transaction.hash, event.transactionLogIndex); - const mint = new MintEvent(mintId); mint.amountMantissa = event.params.mintTokens; mint.to = event.params.minter; @@ -122,7 +25,6 @@ export function createMintEvent(event: E): void { export function createMintBehalfEvent(event: E): void { const mintId = getTransactionId(event.transaction.hash, event.transactionLogIndex); - const mint = new MintEvent(mintId); mint.amountMantissa = event.params.mintTokens; mint.to = event.params.receiver; @@ -136,7 +38,6 @@ export function createMintBehalfEvent(event: E): void { export function createRedeemEvent(event: E): void { const redeemId = getTransactionId(event.transaction.hash, event.transactionLogIndex); - const redeem = new RedeemEvent(redeemId); redeem.amountMantissa = event.params.redeemTokens; redeem.to = event.address; @@ -147,3 +48,54 @@ export function createRedeemEvent(event: E): void { redeem.underlyingAmountMantissa = event.params.redeemAmount; redeem.save(); } + +export function createBorrowEvent(event: E, underlyingAddress: Bytes): void { + const borrowID = getTransactionId(event.transaction.hash, event.transactionLogIndex); + const borrow = new BorrowEvent(borrowID); + borrow.amountMantissa = event.params.borrowAmount; + borrow.accountBorrowsMantissa = event.params.accountBorrows; + borrow.borrower = event.params.borrower; + borrow.blockNumber = event.block.number.toI32(); + borrow.blockTime = event.block.timestamp.toI32(); + borrow.underlyingAddress = underlyingAddress; + borrow.save(); +} + +export function createRepayEvent(event: E, underlyingAddress: Bytes): void { + const repayID = getTransactionId(event.transaction.hash, event.transactionLogIndex); + const repay = new RepayEvent(repayID); + repay.amountMantissa = event.params.repayAmount; + repay.accountBorrowsMantissa = event.params.accountBorrows; + repay.borrower = event.params.borrower; + repay.blockNumber = event.block.number.toI32(); + repay.blockTime = event.block.timestamp.toI32(); + repay.underlyingAddress = underlyingAddress; + repay.payer = event.params.payer; + repay.save(); +} + +export function createLiquidationEvent(event: E, underlyingAddress: Bytes): void { + const liquidationID = getTransactionId(event.transaction.hash, event.transactionLogIndex); + const liquidation = new LiquidationEvent(liquidationID); + liquidation.amountMantissa = event.params.seizeTokens; + liquidation.to = event.params.liquidator; + liquidation.from = event.params.borrower; + liquidation.blockNumber = event.block.number.toI32(); + liquidation.blockTime = event.block.timestamp.toI32(); + liquidation.underlyingRepaidAddress = underlyingAddress; + liquidation.underlyingRepayAmountMantissa = event.params.repayAmount; + liquidation.vTokenCollateralAddress = event.params.vTokenCollateral; + liquidation.save(); +} + +export function createTransferEvent(event: E): void { + const transferId = getTransactionId(event.transaction.hash, event.transactionLogIndex); + const transfer = new TransferEvent(transferId); + transfer.amountMantissa = event.params.amount; + transfer.to = event.params.to; + transfer.from = event.params.from; + transfer.blockNumber = event.block.number.toI32(); + transfer.blockTime = event.block.timestamp.toI32(); + transfer.vTokenAddress = event.address; + transfer.save(); +} diff --git a/subgraphs/venus/src/operations/get.ts b/subgraphs/venus/src/operations/get.ts index 57159a22..cc84d2ff 100644 --- a/subgraphs/venus/src/operations/get.ts +++ b/subgraphs/venus/src/operations/get.ts @@ -1,13 +1,11 @@ import { Address, log } from '@graphprotocol/graph-ts'; import { Market } from '../../generated/schema'; -import { getMarketId } from '../utilities/ids'; export const getMarket = (vTokenAddress: Address): Market | null => { - const id = getMarketId(vTokenAddress); - const market = Market.load(id); + const market = Market.load(vTokenAddress.toHexString()); if (!market) { - log.error('Market {} not found', [id]); + log.error('Market {} not found', [vTokenAddress.toHexString()]); } return market; }; diff --git a/subgraphs/venus/src/operations/getOrCreate.ts b/subgraphs/venus/src/operations/getOrCreate.ts index 0950c18a..be38066c 100644 --- a/subgraphs/venus/src/operations/getOrCreate.ts +++ b/subgraphs/venus/src/operations/getOrCreate.ts @@ -1,51 +1,201 @@ -import { BigInt, Bytes } from '@graphprotocol/graph-ts'; +import { Address, ethereum, log } from '@graphprotocol/graph-ts'; import { Comptroller as ComptrollerContract } from '../../generated/Comptroller/Comptroller'; -import { Comptroller } from '../../generated/schema'; +import { Account, AccountVToken, Comptroller, Market } from '../../generated/schema'; import { AccountVTokenTransaction } from '../../generated/schema'; -import { comptrollerAddress } from '../constants/addresses'; +import { + VToken as VTokenTemplate, + VTokenUpdatedEvents as VTokenUpdatedEventsTemplate, +} from '../../generated/templates'; +import { BEP20 } from '../../generated/templates/VToken/BEP20'; +import { VToken } from '../../generated/templates/VToken/VToken'; +import { zeroBigInt32 } from '../constants'; +import { comptrollerAddress, nullAddress } from '../constants/addresses'; +import { + valueOrNotAvailableAddressIfReverted, + valueOrNotAvailableIntIfReverted, +} from '../utilities'; +import { exponentToBigInt } from '../utilities/exponentToBigInt'; +import { getUnderlyingPrice } from '../utilities/getUnderlyingPrice'; +import { getAccountVTokenId, getAccountVTokenTransactionId } from '../utilities/ids'; +import { getMarket } from './get'; -export const getOrCreateComptroller = (): Comptroller => { +export function getOrCreateComptroller(): Comptroller { let comptroller = Comptroller.load(comptrollerAddress.toHexString()); - if (comptroller) { - return comptroller; + if (!comptroller) { + comptroller = new Comptroller(comptrollerAddress.toHexString()); + const comptrollerContract = ComptrollerContract.bind(comptrollerAddress); + comptroller.priceOracle = comptrollerContract.oracle(); + comptroller.closeFactor = comptrollerContract.closeFactorMantissa(); + comptroller.liquidationIncentive = comptrollerContract.liquidationIncentiveMantissa(); + comptroller.maxAssets = comptrollerContract.maxAssets(); + comptroller.save(); } + return comptroller; +} - comptroller = new Comptroller(comptrollerAddress.toHexString()); - const comptrollerContract = ComptrollerContract.bind(comptrollerAddress); +export function getOrCreateMarket(marketAddress: Address, event: ethereum.Event): Market { + const vTokenContract = VToken.bind(marketAddress); + // @todo add and use market id utility + let market = getMarket(marketAddress); + if (!market) { + market = new Market(marketAddress.toHexString()); + market.name = vTokenContract.name(); + market.symbol = vTokenContract.symbol(); + market.vTokenDecimals = vTokenContract.decimals(); - comptroller.priceOracle = comptrollerContract.oracle(); - comptroller.closeFactor = comptrollerContract.closeFactorMantissa(); - comptroller.liquidationIncentive = comptrollerContract.liquidationIncentiveMantissa(); - comptroller.maxAssets = comptrollerContract.maxAssets(); + // It is vBNB, which has a slightly different interface + if (market.symbol == 'vBNB') { + market.underlyingAddress = nullAddress; + market.underlyingDecimals = 18; + market.underlyingName = 'BNB'; + market.underlyingSymbol = 'BNB'; + } else { + market.underlyingAddress = vTokenContract.underlying(); + log.debug('[createMarket] market underlying address: {}', [ + market.underlyingAddress.toHexString(), + ]); + const underlyingContract = BEP20.bind(Address.fromBytes(market.underlyingAddress)); + market.underlyingDecimals = underlyingContract.decimals(); + market.underlyingName = underlyingContract.name(); + market.underlyingSymbol = underlyingContract.symbol(); + } - comptroller.save(); - return comptroller; -}; - -export const getOrCreateAccountVTokenTransaction = ( - accountID: string, - txHash: Bytes, - timestamp: BigInt, - block: BigInt, - logIndex: BigInt, -): AccountVTokenTransaction => { - const id = accountID - .concat('-') - .concat(txHash.toHexString()) - .concat('-') - .concat(logIndex.toString()); - let transaction = AccountVTokenTransaction.load(id); + market.interestRateModelAddress = valueOrNotAvailableAddressIfReverted( + vTokenContract.try_interestRateModel(), + 'vBEP20 try_interestRateModel()', + ); + market.reserveFactor = valueOrNotAvailableIntIfReverted( + vTokenContract.try_reserveFactorMantissa(), + 'vBEP20 try_reserveFactorMantissa()', + ); + + market.accrualBlockNumber = 0; + market.totalXvsDistributedMantissa = zeroBigInt32; + market.collateralFactorMantissa = zeroBigInt32; + market.supplierCount = zeroBigInt32; + market.borrowerCount = zeroBigInt32; + market.borrowerCountAdjusted = zeroBigInt32; + + // Dynamically index all new listed tokens + VTokenTemplate.create(marketAddress); + VTokenUpdatedEventsTemplate.create(marketAddress); + } + + const contractAccrualBlockNumber = vTokenContract.accrualBlockNumber().toI32(); + if (market.accrualBlockNumber < contractAccrualBlockNumber) { + market.accrualBlockNumber = contractAccrualBlockNumber; + market.blockTimestamp = event.block.timestamp.toI32(); + market.underlyingPriceCents = + market.symbol == 'vBNB' + ? zeroBigInt32 + : getUnderlyingPrice(market.id, market.underlyingDecimals); + + /* Exchange rate explanation + In Practice + - If you call the vDAI contract on bscscan it comes back (2.0 * 10^26) + - If you call the vUSDC contract on bscscan it comes back (2.0 * 10^14) + - The real value is ~0.02. So vDAI is off by 10^28, and vUSDC 10^16 + How to calculate for tokens with different decimals + - Must div by tokenDecimals, 10^market.underlyingDecimals + - Must multiply by vtokenDecimals, 10^8 + - Must div by mantissa, 10^18 + */ + + // Must convert to BigDecimal, and remove 10^18 that is used for Exp in Venus Solidity + market.exchangeRateMantissa = valueOrNotAvailableIntIfReverted( + vTokenContract.try_exchangeRateStored(), + 'vBEP20 try_exchangeRateStored()', + ); + market.borrowRateMantissa = valueOrNotAvailableIntIfReverted( + vTokenContract.try_borrowRatePerBlock(), + 'vBEP20 try_borrowRatePerBlock()', + ); + + // This fails on only the first call to cZRX. It is unclear why, but otherwise it works. + // So we handle it like this. + market.supplyRateMantissa = valueOrNotAvailableIntIfReverted( + vTokenContract.try_supplyRatePerBlock(), + 'vBEP20 try_supplyRatePerBlock()', + ); + market.totalSupplyMantissa = vTokenContract + .totalSupply() + .times(market.exchangeRateMantissa) + .div(exponentToBigInt(18)); + market.borrowIndexMantissa = vTokenContract.borrowIndex(); + market.reservesMantissa = vTokenContract.totalReserves(); + market.totalBorrowsMantissa = vTokenContract.totalBorrows(); + market.cashMantissa = vTokenContract.getCash(); + } + market.save(); + + return market; +} + +export function getOrCreateAccount(accountId: string): Account { + let account = Account.load(accountId); + if (!account) { + account = new Account(accountId); + account.countLiquidated = 0; + account.countLiquidator = 0; + account.hasBorrowed = false; + account.save(); + } + return account; +} + +export function getOrCreateAccountVToken( + marketId: string, + marketSymbol: string, + accountId: string, + event: ethereum.Event, +): AccountVToken { + const accountVTokenId = getAccountVTokenId( + Address.fromString(marketId), + Address.fromString(accountId), + ); + let accountVToken = AccountVToken.load(accountVTokenId); + if (!accountVToken) { + accountVToken = new AccountVToken(accountVTokenId); + accountVToken.market = marketId; + accountVToken.symbol = marketSymbol; + accountVToken.account = accountId; + accountVToken.accrualBlockNumber = event.block.number; + // we need to set an initial real onchain value to this otherwise it will never be accurate + const vTokenContract = VToken.bind(Address.fromString(marketId)); + accountVToken.vTokenBalanceMantissa = vTokenContract.balanceOf(Address.fromString(accountId)); + + accountVToken.totalUnderlyingSuppliedMantissa = zeroBigInt32; + accountVToken.totalUnderlyingRedeemedMantissa = zeroBigInt32; + accountVToken.accountBorrowIndexMantissa = zeroBigInt32; + accountVToken.totalUnderlyingBorrowedMantissa = zeroBigInt32; + accountVToken.totalUnderlyingRepaidMantissa = zeroBigInt32; + accountVToken.storedBorrowBalanceMantissa = zeroBigInt32; + accountVToken.enteredMarket = false; + accountVToken.save(); + } + return accountVToken; +} + +export function getOrCreateAccountVTokenTransaction( + accountVTokenId: string, + event: ethereum.Event, +): AccountVTokenTransaction { + const id = getAccountVTokenTransactionId( + accountVTokenId, + event.transaction.hash, + event.transactionLogIndex, + ); + let transaction = AccountVTokenTransaction.load(id); if (transaction == null) { transaction = new AccountVTokenTransaction(id); - transaction.account = accountID; - transaction.tx_hash = txHash; // eslint-disable-line @typescript-eslint/naming-convention - transaction.timestamp = timestamp; - transaction.block = block; - transaction.logIndex = logIndex; + transaction.account = accountVTokenId; + transaction.tx_hash = event.transaction.hash; // eslint-disable-line @typescript-eslint/naming-convention + transaction.logIndex = event.transactionLogIndex; + transaction.timestamp = event.block.timestamp; + transaction.block = event.block.number; transaction.save(); } - - return transaction as AccountVTokenTransaction; -}; + return transaction; +} diff --git a/subgraphs/venus/src/operations/update.ts b/subgraphs/venus/src/operations/update.ts deleted file mode 100644 index d1eb4065..00000000 --- a/subgraphs/venus/src/operations/update.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { Address, BigInt, Bytes, log } from '@graphprotocol/graph-ts'; - -import { AccountVToken, Market } from '../../generated/schema'; -import { VToken } from '../../generated/templates/VToken/VToken'; -import { zeroBigInt32 } from '../constants'; -import { createMarket } from '../operations/create'; -import { getExchangeRate } from '../utilities'; -import { exponentToBigInt } from '../utilities/exponentToBigInt'; -import { getUnderlyingPrice } from '../utilities/getUnderlyingPrice'; -import { createAccountVToken } from './create'; -import { getOrCreateAccountVTokenTransaction } from './getOrCreate'; - -export const updateCommonVTokenStats = ( - marketId: string, - marketSymbol: string, - accountId: string, - txHash: Bytes, - timestamp: BigInt, - blockNumber: BigInt, - logIndex: BigInt, -): AccountVToken => { - const accountVTokenId = marketId.concat('-').concat(accountId); - let accountVToken = AccountVToken.load(accountVTokenId); - if (accountVToken == null) { - accountVToken = createAccountVToken(accountVTokenId, marketSymbol, accountId, marketId); - } - getOrCreateAccountVTokenTransaction(accountVTokenId, txHash, timestamp, blockNumber, logIndex); - accountVToken.accrualBlockNumber = blockNumber; - return accountVToken as AccountVToken; -}; - -export const updateMarket = ( - marketAddress: Address, - blockNumber: i32, - blockTimestamp: i32, -): Market => { - const marketId = marketAddress.toHexString(); - let market = Market.load(marketId) as Market; - if (market == null) { - market = createMarket(marketId); - } - - // Only updateMarket if it has not been updated this block - if (market.accrualBlockNumber != blockNumber) { - const marketAddress = Address.fromString(market.id); - const contract = VToken.bind(marketAddress); - market.accrualBlockNumber = contract.accrualBlockNumber().toI32(); - market.blockTimestamp = blockTimestamp; - - const underlyingPriceCents = getUnderlyingPrice(market.id, market.underlyingDecimals); - market.underlyingPriceCents = underlyingPriceCents; - - /* Exchange rate explanation - In Practice - - If you call the vDAI contract on bscscan it comes back (2.0 * 10^26) - - If you call the vUSDC contract on bscscan it comes back (2.0 * 10^14) - - The real value is ~0.02. So vDAI is off by 10^28, and vUSDC 10^16 - How to calculate for tokens with different decimals - - Must div by tokenDecimals, 10^market.underlyingDecimals - - Must multiply by vtokenDecimals, 10^8 - - Must div by mantissa, 10^18 - */ - const exchangeRateStored = getExchangeRate(marketAddress); - market.exchangeRateMantissa = exchangeRateStored; - - const totalSupplyVTokensMantissa = contract.totalSupply(); - market.totalSupplyMantissa = totalSupplyVTokensMantissa - .times(exchangeRateStored) - .div(exponentToBigInt(18)); - - market.borrowIndexMantissa = contract.borrowIndex(); - - market.reservesMantissa = contract.totalReserves(); - market.totalBorrowsMantissa = contract.totalBorrows(); - - market.cashMantissa = contract.getCash(); - - // Must convert to BigDecimal, and remove 10^18 that is used for Exp in Venus Solidity - const borrowRatePerBlock = contract.try_borrowRatePerBlock(); - if (borrowRatePerBlock.reverted) { - log.error('***CALL FAILED*** : vBEP20 supplyRatePerBlock() reverted', []); - market.exchangeRateMantissa = zeroBigInt32; - } else { - market.borrowRateMantissa = borrowRatePerBlock.value; - } - - // This fails on only the first call to cZRX. It is unclear why, but otherwise it works. - // So we handle it like this. - const supplyRatePerBlock = contract.try_supplyRatePerBlock(); - if (supplyRatePerBlock.reverted) { - log.info('***CALL FAILED*** : vBEP20 supplyRatePerBlock() reverted', []); - market.supplyRateMantissa = zeroBigInt32; - } else { - market.supplyRateMantissa = supplyRatePerBlock.value; - } - market.save(); - } - return market as Market; -}; diff --git a/subgraphs/venus/src/utilities/getExchangeRate.ts b/subgraphs/venus/src/utilities/getExchangeRate.ts deleted file mode 100644 index 94ed2e3a..00000000 --- a/subgraphs/venus/src/utilities/getExchangeRate.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Address, BigInt, log } from '@graphprotocol/graph-ts'; - -import { VToken } from '../../generated/templates/VToken/VToken'; -import valueOrNotAvailableIntIfReverted from './valueOrNotAvailableIntIfReverted'; - -export function getExchangeRate(marketAddress: Address): BigInt { - const vTokenContract = VToken.bind(marketAddress); - const tryExchangeRateStored = vTokenContract.try_exchangeRateStored(); - if (tryExchangeRateStored.reverted) { - log.warning('Failed to get exchange rate for {}', [marketAddress.toHexString()]); - } - return valueOrNotAvailableIntIfReverted(tryExchangeRateStored); -} - -export default getExchangeRate; diff --git a/subgraphs/venus/src/utilities/getTokenPriceCents.ts b/subgraphs/venus/src/utilities/getTokenPriceCents.ts index 117a0bce..fa7636f7 100644 --- a/subgraphs/venus/src/utilities/getTokenPriceCents.ts +++ b/subgraphs/venus/src/utilities/getTokenPriceCents.ts @@ -25,6 +25,7 @@ export function getTokenPriceCents(eventAddress: Address, underlyingDecimals: i3 const oracle2 = PriceOracle.bind(oracleAddress); const oracleUnderlyingPrice = valueOrNotAvailableIntIfReverted( oracle2.try_getUnderlyingPrice(eventAddress), + 'PriceOracle try_getUnderlyingPrice', ); if (oracleUnderlyingPrice.equals(BigInt.zero())) { return oracleUnderlyingPrice; diff --git a/subgraphs/venus/src/utilities/ids.ts b/subgraphs/venus/src/utilities/ids.ts index fc3748ea..42cbf008 100644 --- a/subgraphs/venus/src/utilities/ids.ts +++ b/subgraphs/venus/src/utilities/ids.ts @@ -6,9 +6,6 @@ const SEPERATOR = '-'; const joinIds = (idArray: Array): string => idArray.join(SEPERATOR); -export const getMarketId = (vTokenAddress: Address): string => - joinIds([vTokenAddress.toHexString()]); - export const getAccountVTokenId = (marketAddress: Address, accountAddress: Address): string => joinIds([marketAddress.toHexString(), accountAddress.toHexString()]); @@ -16,11 +13,10 @@ export const getTransactionId = (transactionHash: Bytes, logIndex: BigInt): stri joinIds([transactionHash.toHexString(), logIndex.toString()]); export const getAccountVTokenTransactionId = ( - accountAddress: Address, + accountVTokenId: string, transactionHash: Bytes, logIndex: BigInt, -): string => - joinIds([accountAddress.toHexString(), transactionHash.toHexString(), logIndex.toString()]); +): string => joinIds([accountVTokenId, transactionHash.toHexString(), logIndex.toString()]); export const getMarketActionId = (vTokenAddress: Address, action: i32): string => { const actionString = Actions[action]; diff --git a/subgraphs/venus/src/utilities/index.ts b/subgraphs/venus/src/utilities/index.ts index f549fec2..79e62d9d 100644 --- a/subgraphs/venus/src/utilities/index.ts +++ b/subgraphs/venus/src/utilities/index.ts @@ -1,5 +1,5 @@ export { getTokenPriceCents } from './getTokenPriceCents'; export { exponentToBigDecimal } from './exponentToBigDecimal'; export { getUnderlyingPrice } from './getUnderlyingPrice'; -export { default as valueOrNotAvailableIntIfReverted } from './valueOrNotAvailableIntIfReverted'; -export { default as getExchangeRate } from './getExchangeRate'; +export { valueOrNotAvailableAddressIfReverted } from './valueOrNotAvailableAddressIfReverted'; +export { valueOrNotAvailableIntIfReverted } from './valueOrNotAvailableIntIfReverted'; diff --git a/subgraphs/venus/src/utilities/valueOrNotAvailableAddressIfReverted.ts b/subgraphs/venus/src/utilities/valueOrNotAvailableAddressIfReverted.ts new file mode 100644 index 00000000..51f8fb41 --- /dev/null +++ b/subgraphs/venus/src/utilities/valueOrNotAvailableAddressIfReverted.ts @@ -0,0 +1,15 @@ +import { Address, ethereum, log } from '@graphprotocol/graph-ts'; + +import { nullAddress } from '../constants/addresses'; + +// checks if a call reverted, in case it is we return 0x000.. to indicate the wanted value is not available +export function valueOrNotAvailableAddressIfReverted( + callResult: ethereum.CallResult
, + callName: string, +): Address { + if (callResult.reverted) { + log.error('***CALL FAILED*** : {} reverted', [callName]); + return nullAddress; + } + return callResult.value; +} diff --git a/subgraphs/venus/src/utilities/valueOrNotAvailableIntIfReverted.ts b/subgraphs/venus/src/utilities/valueOrNotAvailableIntIfReverted.ts index b51b0c2c..05262841 100644 --- a/subgraphs/venus/src/utilities/valueOrNotAvailableIntIfReverted.ts +++ b/subgraphs/venus/src/utilities/valueOrNotAvailableIntIfReverted.ts @@ -1,10 +1,17 @@ -import { BigInt, ethereum } from '@graphprotocol/graph-ts'; +import { BigInt, ethereum, log } from '@graphprotocol/graph-ts'; import { NOT_AVAILABLE_BIG_INT } from '../constants'; // checks if a call reverted, in case it is we return -1 to indicate the wanted value is not available -function valueOrNotAvailableIntIfReverted(callResult: ethereum.CallResult): BigInt { - return callResult.reverted ? NOT_AVAILABLE_BIG_INT : callResult.value; +export function valueOrNotAvailableIntIfReverted( + callResult: ethereum.CallResult, + callName: string, +): BigInt { + if (callResult.reverted) { + log.error('***CALL FAILED*** : {} reverted', [callName]); + return NOT_AVAILABLE_BIG_INT; + } + return callResult.value; } export default valueOrNotAvailableIntIfReverted; diff --git a/subgraphs/venus/template.yaml b/subgraphs/venus/template.yaml index 1a8562e6..f54646c0 100644 --- a/subgraphs/venus/template.yaml +++ b/subgraphs/venus/template.yaml @@ -1,4 +1,4 @@ -specVersion: 0.0.2 +specVersion: 0.0.4 description: Venus is an open-source protocol for algorithmic, efficient Money Markets on the BSC blockchain. repository: https://github.com/VenusProtocol/subgraphs schema: diff --git a/subgraphs/venus/tests/Comptroller.test.ts b/subgraphs/venus/tests/Comptroller.test.ts index b6b0167e..4a8c76b2 100644 --- a/subgraphs/venus/tests/Comptroller.test.ts +++ b/subgraphs/venus/tests/Comptroller.test.ts @@ -66,8 +66,8 @@ describe('handleMarketListing', () => { assertMarketDocument('symbol', 'vBNB'); assertMarketDocument('totalBorrowsMantissa', '0'); assertMarketDocument('totalSupplyMantissa', '0'); - assertMarketDocument('accrualBlockNumber', '0'); - assertMarketDocument('blockTimestamp', '0'); + assertMarketDocument('accrualBlockNumber', '100'); + assertMarketDocument('blockTimestamp', '1'); assertMarketDocument('borrowIndexMantissa', '0'); assertMarketDocument('reserveFactor', '100'); }); diff --git a/subgraphs/venus/tests/mocks.ts b/subgraphs/venus/tests/mocks.ts index 48e6d6d5..0ece187e 100644 --- a/subgraphs/venus/tests/mocks.ts +++ b/subgraphs/venus/tests/mocks.ts @@ -8,7 +8,7 @@ export const mockPriceOracleAddress = Address.fromString( ); export const createVBep20AndUnderlyingMock = ( - contractAddress: Address, + vTokenContractAddress: Address, underlyingAddress: Address, name: string, symbol: string, @@ -17,30 +17,75 @@ export const createVBep20AndUnderlyingMock = ( interestRateModelAddress: Address, ): void => { // vBep20 - createMockedFunction(contractAddress, 'underlying', 'underlying():(address)').returns([ + createMockedFunction(vTokenContractAddress, 'underlying', 'underlying():(address)').returns([ ethereum.Value.fromAddress(underlyingAddress), ]); - createMockedFunction(contractAddress, 'name', 'name():(string)').returns([ + createMockedFunction(vTokenContractAddress, 'name', 'name():(string)').returns([ ethereum.Value.fromString(`Venus ${name}`), ]); - createMockedFunction(contractAddress, 'symbol', 'symbol():(string)').returns([ + createMockedFunction(vTokenContractAddress, 'symbol', 'symbol():(string)').returns([ ethereum.Value.fromString(`v${symbol}`), ]); createMockedFunction( - contractAddress, + vTokenContractAddress, 'interestRateModel', 'interestRateModel():(address)', ).returns([ethereum.Value.fromAddress(interestRateModelAddress)]); + createMockedFunction( - contractAddress, + vTokenContractAddress, 'reserveFactorMantissa', 'reserveFactorMantissa():(uint256)', ).returns([ethereum.Value.fromUnsignedBigInt(reserveFactorMantissa)]); - createMockedFunction(contractAddress, 'decimals', 'decimals():(uint8)').returns([ + createMockedFunction(vTokenContractAddress, 'decimals', 'decimals():(uint8)').returns([ ethereum.Value.fromUnsignedBigInt(decimals), ]); + createMockedFunction( + vTokenContractAddress, + 'accrualBlockNumber', + 'accrualBlockNumber():(uint256)', + ).returns([ethereum.Value.fromUnsignedBigInt(BigInt.fromString('100'))]); + + createMockedFunction( + vTokenContractAddress, + 'exchangeRateStored', + 'exchangeRateStored():(uint256)', + ).returns([ethereum.Value.fromUnsignedBigInt(BigInt.fromString('0'))]); + + createMockedFunction( + vTokenContractAddress, + 'borrowRatePerBlock', + 'borrowRatePerBlock():(uint256)', + ).returns([ethereum.Value.fromI32(0)]); + + createMockedFunction( + vTokenContractAddress, + 'supplyRatePerBlock', + 'supplyRatePerBlock():(uint256)', + ).returns([ethereum.Value.fromI32(0)]); + + createMockedFunction(vTokenContractAddress, 'totalSupply', 'totalSupply():(uint256)').returns([ + ethereum.Value.fromUnsignedBigInt(BigInt.fromString('0')), + ]); + + createMockedFunction(vTokenContractAddress, 'borrowIndex', 'borrowIndex():(uint256)').returns([ + ethereum.Value.fromUnsignedBigInt(BigInt.fromString('0')), + ]); + + createMockedFunction(vTokenContractAddress, 'totalReserves', 'totalReserves():(uint256)').returns( + [ethereum.Value.fromUnsignedBigInt(BigInt.fromString('0'))], + ); + + createMockedFunction(vTokenContractAddress, 'totalBorrows', 'totalBorrows():(uint256)').returns([ + ethereum.Value.fromUnsignedBigInt(BigInt.fromString('0')), + ]); + + createMockedFunction(vTokenContractAddress, 'getCash', 'getCash():(uint256)').returns([ + ethereum.Value.fromUnsignedBigInt(BigInt.fromString('0')), + ]); + // Underlying createMockedFunction(underlyingAddress, 'decimals', 'decimals():(uint8)').returns([ ethereum.Value.fromUnsignedBigInt(decimals), @@ -74,6 +119,10 @@ export const createComptrollerMock = (comptrollerAddress: Address): void => { ethereum.Value.fromI32(10), ]); + createMockedFunction(comptrollerAddress, 'oracle', 'oracle():(address)').returns([ + ethereum.Value.fromAddress(mockPriceOracleAddress), + ]); + createMockedFunction( mockPriceOracleAddress, 'getUnderlyingPrice', diff --git a/yarn.lock b/yarn.lock index 3d42f555..75524ecd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1631,12 +1631,14 @@ __metadata: languageName: node linkType: hard -"@graphprotocol/graph-cli@npm:0.56.0": - version: 0.56.0 - resolution: "@graphprotocol/graph-cli@npm:0.56.0" +"@graphprotocol/graph-cli@npm:^0.67.1": + version: 0.67.1 + resolution: "@graphprotocol/graph-cli@npm:0.67.1" dependencies: "@float-capital/float-subgraph-uncrashable": ^0.0.0-alpha.4 "@oclif/core": 2.8.6 + "@oclif/plugin-autocomplete": ^2.3.6 + "@oclif/plugin-not-found": ^2.4.0 "@whatwg-node/fetch": ^0.8.4 assemblyscript: 0.19.23 binary-install-raw: 0.0.13 @@ -1653,7 +1655,7 @@ __metadata: ipfs-http-client: 55.0.0 jayson: 4.0.0 js-yaml: 3.14.1 - prettier: 1.19.1 + prettier: 3.0.3 request: 2.88.2 semver: 7.4.0 sync-request: 6.1.0 @@ -1663,16 +1665,16 @@ __metadata: yaml: 1.10.2 bin: graph: bin/run - checksum: 485eadfbbda06096cbca1573cf28a786dc9f4b5cc899475ea9b3b5ad8c2e3b477415257d885631bdb958a13cd753230ebf49bdcf2748c37005121b660ab5efba + checksum: fdd767cc90ed595cb6becd518ffd99fff93447eac8436504d91348a4171de238b449c19374217e517aa31d5467d84f2d59bcfa13dc91a544670af07b7c51ea3b languageName: node linkType: hard -"@graphprotocol/graph-ts@npm:0.31.0": - version: 0.31.0 - resolution: "@graphprotocol/graph-ts@npm:0.31.0" +"@graphprotocol/graph-ts@npm:^0.32.0": + version: 0.32.0 + resolution: "@graphprotocol/graph-ts@npm:0.32.0" dependencies: assemblyscript: 0.19.10 - checksum: 20394b17d3241a662f0b2efebdc02f44e4e0814fdbb09b4cac0c84b84a173798325840bc1daf1d11ebaf27b1b3e13ff6e2766567c755cb77c1321ba1462fbecc + checksum: 9376ad625d3e439c9026c6aac15c82b43bb704c99469649e758bd27e68c3de3dcf36381a01ca76fea073f9e4ec507bccfa9ee4bb2b82bf67404419855efc735f languageName: node linkType: hard @@ -3323,6 +3325,64 @@ __metadata: languageName: node linkType: hard +"@oclif/core@npm:^2.15.0": + version: 2.15.0 + resolution: "@oclif/core@npm:2.15.0" + dependencies: + "@types/cli-progress": ^3.11.0 + ansi-escapes: ^4.3.2 + ansi-styles: ^4.3.0 + cardinal: ^2.1.1 + chalk: ^4.1.2 + clean-stack: ^3.0.1 + cli-progress: ^3.12.0 + debug: ^4.3.4 + ejs: ^3.1.8 + get-package-type: ^0.1.0 + globby: ^11.1.0 + hyperlinker: ^1.0.0 + indent-string: ^4.0.0 + is-wsl: ^2.2.0 + js-yaml: ^3.14.1 + natural-orderby: ^2.0.3 + object-treeify: ^1.1.33 + password-prompt: ^1.1.2 + slice-ansi: ^4.0.0 + string-width: ^4.2.3 + strip-ansi: ^6.0.1 + supports-color: ^8.1.1 + supports-hyperlinks: ^2.2.0 + ts-node: ^10.9.1 + tslib: ^2.5.0 + widest-line: ^3.1.0 + wordwrap: ^1.0.0 + wrap-ansi: ^7.0.0 + checksum: a4ef8ad00d9bc7cb48e5847bad7def6947f913875f4b0ecec65ab423a3c2a82c87df173c709c3c25396d545f60d20d17d562c474f66230d76de43061ce22ba90 + languageName: node + linkType: hard + +"@oclif/plugin-autocomplete@npm:^2.3.6": + version: 2.3.10 + resolution: "@oclif/plugin-autocomplete@npm:2.3.10" + dependencies: + "@oclif/core": ^2.15.0 + chalk: ^4.1.0 + debug: ^4.3.4 + checksum: 294f21679a1dfec7f7cd593e704f160a8137733215b58158cb4bf0b6855684f4e27e0df118bf803e894fc52e7f1184ecae3026ce96057a19c6dbdd9318c7e2f9 + languageName: node + linkType: hard + +"@oclif/plugin-not-found@npm:^2.4.0": + version: 2.4.3 + resolution: "@oclif/plugin-not-found@npm:2.4.3" + dependencies: + "@oclif/core": ^2.15.0 + chalk: ^4 + fast-levenshtein: ^3.0.0 + checksum: a7452e4d4b868411856dd3a195609af0757a8a171fbcc17f4311d8b04764e37f4c6d32dd1d9078b166452836e5fa7c42996ffb444016e466f92092c46a2606fb + languageName: node + linkType: hard + "@openzeppelin/contracts-upgradeable@npm:^4.7.3, @openzeppelin/contracts-upgradeable@npm:^4.8.0, @openzeppelin/contracts-upgradeable@npm:^4.8.3": version: 4.9.3 resolution: "@openzeppelin/contracts-upgradeable@npm:4.9.3" @@ -6068,7 +6128,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^4.0.0, chalk@npm:^4.0.2, chalk@npm:^4.1.0, chalk@npm:^4.1.2": +"chalk@npm:^4, chalk@npm:^4.0.0, chalk@npm:^4.0.2, chalk@npm:^4.1.0, chalk@npm:^4.1.2": version: 4.1.2 resolution: "chalk@npm:4.1.2" dependencies: @@ -8180,6 +8240,15 @@ __metadata: languageName: node linkType: hard +"fast-levenshtein@npm:^3.0.0": + version: 3.0.0 + resolution: "fast-levenshtein@npm:3.0.0" + dependencies: + fastest-levenshtein: ^1.0.7 + checksum: 02732ba6c656797ca7e987c25f3e53718c8fcc39a4bfab46def78eef7a8729eb629632d4a7eca4c27a33e10deabffa9984839557e18a96e91ecf7ccaeedb9890 + languageName: node + linkType: hard + "fast-querystring@npm:^1.1.1": version: 1.1.2 resolution: "fast-querystring@npm:1.1.2" @@ -8198,6 +8267,13 @@ __metadata: languageName: node linkType: hard +"fastest-levenshtein@npm:^1.0.7": + version: 1.0.16 + resolution: "fastest-levenshtein@npm:1.0.16" + checksum: a78d44285c9e2ae2c25f3ef0f8a73f332c1247b7ea7fb4a191e6bb51aa6ee1ef0dfb3ed113616dcdc7023e18e35a8db41f61c8d88988e877cf510df8edafbc71 + languageName: node + linkType: hard + "fastq@npm:^1.6.0": version: 1.15.0 resolution: "fastq@npm:1.15.0" @@ -12953,12 +13029,12 @@ __metadata: languageName: node linkType: hard -"prettier@npm:1.19.1": - version: 1.19.1 - resolution: "prettier@npm:1.19.1" +"prettier@npm:3.0.3": + version: 3.0.3 + resolution: "prettier@npm:3.0.3" bin: - prettier: ./bin-prettier.js - checksum: bc78219e0f8173a808f4c6c8e0a137dd8ebd4fbe013e63fe1a37a82b48612f17b8ae8e18a992adf802ee2cf7428f14f084e7c2846ca5759cf4013c6e54810e1f + prettier: bin/prettier.cjs + checksum: e10b9af02b281f6c617362ebd2571b1d7fc9fb8a3bd17e371754428cda992e5e8d8b7a046e8f7d3e2da1dcd21aa001e2e3c797402ebb6111b5cd19609dd228e0 languageName: node linkType: hard @@ -14619,8 +14695,8 @@ __metadata: resolution: "subgraphs@workspace:." dependencies: "@graphprotocol/client-cli": ^3.0.0 - "@graphprotocol/graph-cli": 0.56.0 - "@graphprotocol/graph-ts": 0.31.0 + "@graphprotocol/graph-cli": ^0.67.1 + "@graphprotocol/graph-ts": ^0.32.0 "@nomicfoundation/hardhat-chai-matchers": ^1.0.3 "@nomicfoundation/hardhat-network-helpers": ^1.0.4 "@nomicfoundation/hardhat-toolbox": ^1.0.2