diff --git a/schema.graphql b/schema.graphql index 101073e..335d87b 100644 --- a/schema.graphql +++ b/schema.graphql @@ -153,6 +153,8 @@ type Bucket @entity { lpb: BigDecimal! # list of lends associated with the bucket lends: [Lend!]! + # list of PositionLends associated with the bucket + positionLends: [PositionLend!]! } # lend occurs per bucket in a pool @@ -234,6 +236,9 @@ type Account @entity { # amount of tokens delegated to this account tokensDelegated: BigDecimal! + # positions associated with the account + positions: [Position!]! + # total number of transactions sent by the account txCount: BigInt! } @@ -707,12 +712,24 @@ type MergeOrRemoveCollateralNFT @entity(immutable: true) { # # # # # # # # # # # # # # # type Position @entity { - id: Bytes! # byte encoded tokenId - tokenId: BigInt # tokenId - indexes: [Lend!]! # list of lends which constitute a position - owner: Bytes! # address of the position owner - pool: Bytes! # address of the pool that the position is associated with - token: Token! # pointer to positionNFT + id: Bytes! # byte encoded tokenId + tokenId: BigInt # tokenId + indexes: [PositionLend!]! # list of PositionLends which constitute a position + owner: Bytes! # address of the position owner + pool: Bytes! # address of the pool that the position is associated with + token: Token! # pointer to LPToken entity + tokenURI: String! # tokenURI of the positionNFT +} + +# created to handle the seperate lpb positions for each bucket index +type PositionLend @entity { + id: Bytes! # $positionId + '|' + $bucketIndex + bucket: Bucket! # pointer to associated Bucket entity + bucketIndex: Int! # index of the bucket with lpb + depositTime: BigInt! # time at which the position was deposited + lpb: BigDecimal! # amount of LPB position has in the bucket + lpbValueInQuote: BigDecimal! # quote equivalent value of LPB in the bucket + tokenId: BigInt! # tokenId of the position } # # # # # # # # # # # # # # diff --git a/src/mappings/base/base-pool.ts b/src/mappings/base/base-pool.ts index 1b93e6d..807d4c0 100644 --- a/src/mappings/base/base-pool.ts +++ b/src/mappings/base/base-pool.ts @@ -1,5 +1,5 @@ import { Address, BigInt, Bytes, ethereum, log } from "@graphprotocol/graph-ts" -import { Account, AddQuoteToken, Bucket, BucketBankruptcy, Flashloan, Lend, LoanStamped, MoveQuoteToken, Pool, RemoveQuoteToken, ReserveAuctionKick, ReserveAuctionTake, Token, TransferLP, UpdateInterestRate } from "../../../generated/schema" +import { Account, AddQuoteToken, Bucket, BucketBankruptcy, Flashloan, Lend, LoanStamped, MoveQuoteToken, Pool, PositionLend, RemoveQuoteToken, ReserveAuctionKick, ReserveAuctionTake, Token, TransferLP, UpdateInterestRate } from "../../../generated/schema" import { AddQuoteToken as AddQuoteTokenERC20Event, MoveQuoteToken as MoveQuoteTokenERC20Event, @@ -22,6 +22,7 @@ import { addReserveAuctionToPool, getBurnInfo, getLenderInfo, isERC20Pool, updat import { incrementTokenTxCount as incrementTokenTxCountERC20Pool } from "../../utils/token-erc20" import { incrementTokenTxCount as incrementTokenTxCountERC721Pool } from "../../utils/token-erc721" import { loadOrCreateReserveAuction, reserveAuctionKickerReward } from "../../utils/pool/reserve-auction" +import { saveOrRemovePositionLend } from "../../utils/position" /*******************************/ @@ -629,15 +630,21 @@ export function _handleBucketBankruptcy(event: ethereum.Event, index: BigInt, lp for (let i = 0; i < bucket.lends.length; i++) { const lendId = bucket.lends[i] const lend = Lend.load(lendId)! - lend.depositTime = bucketBankruptcy.blockTimestamp.plus(ONE_BI) lend.lpb = ZERO_BD - lend.lpbValueInQuote = ZERO_BD updateBucketLends(bucket, lend) updateAccountLends(loadOrCreateAccount(lend.lender), lend) // remove lend from store saveOrRemoveLend(lend) } + // iterate through all bucket positionLends and set positionLend.lpb to zero + for (let i = 0; i < bucket.positionLends.length; i++) { + const positionLendId = bucket.positionLends[i] + const positionLend = PositionLend.load(positionLendId)! + positionLend.lpb = ZERO_BD + saveOrRemovePositionLend(positionLend) + } + // save entities to store pool.save() bucket.save() diff --git a/src/mappings/position-manager.ts b/src/mappings/position-manager.ts index a00fa0d..8ce80ae 100644 --- a/src/mappings/position-manager.ts +++ b/src/mappings/position-manager.ts @@ -1,4 +1,4 @@ -import { Address, log } from "@graphprotocol/graph-ts" +import { Address, BigInt, log } from "@graphprotocol/graph-ts" import { Approval as ApprovalEvent, ApprovalForAll as ApprovalForAllEvent, @@ -12,6 +12,7 @@ import { import { Approval, ApprovalForAll, + Bucket, Burn, MemorializePosition, Mint, @@ -22,10 +23,12 @@ import { import { getBucketId } from "../utils/pool/bucket" import { lpbValueInQuote, saveOrRemoveLend } from "../utils/pool/lend" import { ONE_BI, ZERO_BD } from "../utils/constants" -import { addressToBytes, bigIntArrayToIntArray, wadToDecimal } from "../utils/convert" +import { addressToBytes, bigIntArrayToIntArray, bigIntToBytes, wadToDecimal } from "../utils/convert" import { getLendId, loadOrCreateLend } from "../utils/pool/lend" -import { deletePosition, getPoolForToken, loadOrCreateLPToken, loadOrCreatePosition } from "../utils/position" +import { deletePosition, getPoolForToken, getPositionInfo, getPositionLendId, loadOrCreateLPToken, loadOrCreatePosition, loadOrCreatePositionLend, saveOrRemovePositionLend, updatePositionLends } from "../utils/position" import { getLenderInfo } from "../utils/pool/pool" +import { getTokenURI } from "../utils/token-erc721" +import { loadOrCreateAccount, updateAccountPositions } from "../utils/account" export function handleApproval(event: ApprovalEvent): void { const approval = new Approval( @@ -68,11 +71,19 @@ export function handleBurn(event: BurnEvent): void { burn.blockTimestamp = event.block.timestamp burn.transactionHash = event.transaction.hash + // remove tokenId from account's list of positions + const account = loadOrCreateAccount(burn.lender) + const index = account.positions.indexOf(bigIntToBytes(burn.tokenId)) + const accountPositions = account.positions + if (index != -1) accountPositions.splice(index, 1) + account.positions = accountPositions deletePosition(event.params.tokenId); + account.save() burn.save() } +// Lends are updated in the associated `TransferLP` event export function handleMemorializePosition( event: MemorializePositionEvent ): void { @@ -98,15 +109,25 @@ export function handleMemorializePosition( log.info("handleMemorializePosition for lender {} token {}" , [accountId.toHexString(), memorialize.tokenId.toString()]) - const positionIndexes = position.indexes; for (let i = 0; i < memorialize.indexes.length; i++) { const index = memorialize.indexes[i]; const bucketId = getBucketId(poolAddress, index) - const lendId = getLendId(bucketId, accountId) - // add lend to position - positionIndexes.push(lendId) + const bucket = Bucket.load(bucketId)! + + // create PositionLend entity to track each lpb associated with the position + const positionLendId = getPositionLendId(memorialize.tokenId, BigInt.fromI32(index)) + const positionLend = loadOrCreatePositionLend(memorialize.tokenId, bucketId, index) + const positionInfo = getPositionInfo(memorialize.tokenId, BigInt.fromI32(index)) + //track the lpb and depositTime associated with each lend that was memorialized via RPC call + positionLend.depositTime = positionInfo.depositTime + positionLend.lpb = wadToDecimal(positionInfo.lpb) + positionLend.lpbValueInQuote = lpbValueInQuote(memorialize.pool, index, positionLend.lpb) + positionLend.save() + + // associate positionLend with Bucket and Position + updatePositionLends(positionLend) + bucket.save() } - position.indexes = positionIndexes // save entities to store memorialize.save() @@ -135,8 +156,13 @@ export function handleMint(event: MintEvent): void { position.token = token.id token.txCount = token.txCount.plus(ONE_BI); + // associate the new position with the lender account + const minterAccount = loadOrCreateAccount(mint.lender) + updateAccountPositions(minterAccount, position) + // save entities to store mint.save() + minterAccount.save() position.save() token.save() } @@ -151,36 +177,51 @@ export function handleMoveLiquidity(event: MoveLiquidityEvent): void { moveLiquidity.fromIndex = event.params.fromIndex.toU32() moveLiquidity.toIndex = event.params.toIndex.toU32() - const bucketIdFrom = getBucketId(moveLiquidity.pool, moveLiquidity.fromIndex) - const lendIdFrom = getLendId(bucketIdFrom, moveLiquidity.lender) - const lendFrom = loadOrCreateLend(bucketIdFrom, lendIdFrom, moveLiquidity.pool, moveLiquidity.fromIndex, moveLiquidity.lender) - const bucketIdTo = getBucketId(moveLiquidity.pool, moveLiquidity.toIndex) - const lendIdTo = getLendId(bucketIdTo, moveLiquidity.lender) - const lendTo = loadOrCreateLend(bucketIdTo, lendIdTo, moveLiquidity.pool, moveLiquidity.toIndex, moveLiquidity.lender) - - const lendToInfo = getLenderInfo(moveLiquidity.pool, event.params.toIndex, Address.fromBytes(lendTo.lender)) - lendTo.depositTime = lendToInfo.depositTime - lendTo.lpb = wadToDecimal(lendToInfo.lpBalance) - lendTo.lpbValueInQuote = lpbValueInQuote(moveLiquidity.pool, moveLiquidity.toIndex, lendTo.lpb) - const lpRedeemedFrom = wadToDecimal(event.params.lpRedeemedFrom) - if (lpRedeemedFrom.le(lendFrom.lpb)) { - lendFrom.lpb = lendFrom.lpb.minus(wadToDecimal(event.params.lpRedeemedFrom)) - } else { - log.warning('handleMoveLiquidity: lender {} redeemed more LP ({}) than Lend entity was aware of ({}); resetting to 0', - [moveLiquidity.lender.toHexString(), lpRedeemedFrom.toString(), lendFrom.lpb.toString()]) - lendFrom.lpb = ZERO_BD - } - lendFrom.lpbValueInQuote = lpbValueInQuote(moveLiquidity.pool, moveLiquidity.toIndex, lendFrom.lpb) - saveOrRemoveLend(lendFrom) - lendTo.save() + moveLiquidity.blockNumber = event.block.number + moveLiquidity.blockTimestamp = event.block.timestamp + moveLiquidity.transactionHash = event.transaction.hash + // update Token tx count const token = loadOrCreateLPToken(event.address) token.txCount = token.txCount.plus(ONE_BI); - moveLiquidity.blockNumber = event.block.number - moveLiquidity.blockTimestamp = event.block.timestamp - moveLiquidity.transactionHash = event.transaction.hash + // load from index state + const bucketIdFrom = getBucketId(moveLiquidity.pool, moveLiquidity.fromIndex) + const positionLendFrom = loadOrCreatePositionLend(moveLiquidity.tokenId, bucketIdFrom, moveLiquidity.fromIndex) + const lpRedeemedFrom = wadToDecimal(event.params.lpRedeemedFrom) + // load to index state + const bucketIdTo = getBucketId(moveLiquidity.pool, moveLiquidity.toIndex) + const positionLendTo = loadOrCreatePositionLend(moveLiquidity.tokenId, bucketIdTo, moveLiquidity.toIndex) + const positionLendToInfo = getPositionInfo(moveLiquidity.tokenId, BigInt.fromI32(moveLiquidity.toIndex)) + + // update positionLendTo + positionLendTo.depositTime = positionLendToInfo.depositTime + positionLendTo.lpb = wadToDecimal(positionLendToInfo.lpb) + positionLendTo.lpbValueInQuote = lpbValueInQuote(moveLiquidity.pool, moveLiquidity.toIndex, positionLendTo.lpb) + positionLendTo.save() + + // associate positionLendTo with Bucket and Position if necessary + updatePositionLends(positionLendTo) + + // update PositionLendFrom + // update positionLendFrom lpb + if (lpRedeemedFrom.le(positionLendFrom.lpb)) { + positionLendFrom.lpb = positionLendFrom.lpb.minus(wadToDecimal(event.params.lpRedeemedFrom)) + } else { + log.warning('handleMoveLiquidity: lender {} redeemed more LP ({}) than PositionLend entity was aware of ({}); resetting to 0', + [moveLiquidity.lender.toHexString(), lpRedeemedFrom.toString(), positionLendFrom.lpb.toString()]) + positionLendFrom.lpb = ZERO_BD + } + // update positionLendFrom lpbValueInQuote + if (positionLendFrom.lpb.equals(ZERO_BD)) { + positionLendFrom.lpbValueInQuote = ZERO_BD + } else { + positionLendFrom.lpbValueInQuote = lpbValueInQuote(moveLiquidity.pool, moveLiquidity.fromIndex, positionLendFrom.lpb) + } + + // save entities to store + saveOrRemovePositionLend(positionLendFrom) moveLiquidity.save() token.save() } @@ -204,15 +245,13 @@ export function handleRedeemPosition(event: RedeemPositionEvent): void { log.info("handleRedeemPosition for lender {} token {}" , [accountId.toHexString(), redeem.tokenId.toString()]) + // update positionLend entities for each index const positionIndexes = position.indexes; for (let index = 0; index < redeem.indexes.length; index++) { const bucketId = getBucketId(poolAddress, index) - const lendId = getLendId(bucketId, accountId) - // remove lends from position - const existingIndex = position.indexes.indexOf(lendId) - if (existingIndex != -1) { - positionIndexes.splice(existingIndex, 1) - } + const positionLend = loadOrCreatePositionLend(redeem.tokenId, bucketId, index) + positionLend.lpb = ZERO_BD + saveOrRemovePositionLend(positionLend) } position.indexes = positionIndexes @@ -245,7 +284,22 @@ export function handleTransfer(event: TransferEvent): void { token.txCount = token.txCount.plus(ONE_BI); const position = loadOrCreatePosition(transfer.tokenId) position.owner = event.params.to + position.tokenURI = getTokenURI(event.address, transfer.tokenId) + // remove position from old account + const oldOwnerAccount = loadOrCreateAccount(transfer.from) + const index = oldOwnerAccount.positions.indexOf(bigIntToBytes(transfer.tokenId)) + const accountPositions = oldOwnerAccount.positions + if (index != -1) accountPositions.splice(index, 1) + oldOwnerAccount.positions = accountPositions + + // add position to new account + const newOwnerAccount = loadOrCreateAccount(transfer.to) + updateAccountPositions(newOwnerAccount, position) + + // save entities to store + oldOwnerAccount.save() + newOwnerAccount.save() token.save(); position.save() transfer.save() diff --git a/src/utils/account.ts b/src/utils/account.ts index 4c10820..90e88aa 100644 --- a/src/utils/account.ts +++ b/src/utils/account.ts @@ -1,5 +1,5 @@ import { Address, BigInt, Bytes, store } from "@graphprotocol/graph-ts" -import { Account, Kick, Lend, Loan, Pool, Settle, Take } from "../../generated/schema" +import { Account, Kick, Lend, Loan, Pool, Position, Settle, Take } from "../../generated/schema" import { ZERO_BD, ZERO_BI } from "./constants" @@ -14,6 +14,7 @@ export function loadOrCreateAccount(accountId: Bytes): Account { account.lends = [] account.loans = [] account.pools = [] + account.positions = [] account.reserveAuctions = [] account.settles = [] account.takes = [] @@ -77,6 +78,16 @@ export function updateAccountKicks(account: Account, kick: Kick): void { } } +// update the list of Positions associated with an account, if it hasn't been added already +export function updateAccountPositions(account: Account, position: Position): void { + const positions = account.positions + // get current index of position in account's list of positions + const index = positions.indexOf(position.id) + if (index == -1) { + account.positions = account.positions.concat([position.id]) + } +} + // update the list of reserve auctions interacted with by an account, if it hasn't been added already export function updateAccountReserveAuctions(account: Account, reserveAuctionId: Bytes): void { const reserveAuctions = account.reserveAuctions diff --git a/src/utils/pool/bucket.ts b/src/utils/pool/bucket.ts index 0b45c22..a352bec 100644 --- a/src/utils/pool/bucket.ts +++ b/src/utils/pool/bucket.ts @@ -1,6 +1,6 @@ import { Address, BigDecimal, BigInt, Bytes, dataSource, log } from "@graphprotocol/graph-ts" -import { Bucket, Lend } from "../../../generated/schema" +import { Bucket, Lend, PositionLend } from "../../../generated/schema" import { PoolInfoUtils } from '../../../generated/templates/ERC20Pool/PoolInfoUtils' import { poolInfoUtilsAddressTable, ONE_BD, ZERO_BD } from "../constants" @@ -53,15 +53,16 @@ export function loadOrCreateBucket(poolId: Bytes, bucketId: Bytes, index: u32): // create new bucket if bucket hasn't already been loaded bucket = new Bucket(bucketId) as Bucket - bucket.bucketIndex = index - bucket.bucketPrice = indexToPrice(index) - bucket.poolAddress = poolId.toHexString() - bucket.pool = poolId - bucket.collateral = ZERO_BD - bucket.deposit = ZERO_BD - bucket.exchangeRate = ONE_BD - bucket.lpb = ZERO_BD - bucket.lends = [] + bucket.bucketIndex = index + bucket.bucketPrice = indexToPrice(index) + bucket.poolAddress = poolId.toHexString() + bucket.pool = poolId + bucket.collateral = ZERO_BD + bucket.deposit = ZERO_BD + bucket.exchangeRate = ONE_BD + bucket.lpb = ZERO_BD + bucket.lends = [] + bucket.positionLends = [] } return bucket } diff --git a/src/utils/pool/pool.ts b/src/utils/pool/pool.ts index 76c5638..1acc6c3 100644 --- a/src/utils/pool/pool.ts +++ b/src/utils/pool/pool.ts @@ -1,4 +1,4 @@ -import { BigDecimal, BigInt, Bytes, Address, dataSource } from '@graphprotocol/graph-ts' +import { BigDecimal, BigInt, Bytes, Address, dataSource, log } from '@graphprotocol/graph-ts' import { LiquidationAuction, Pool, ReserveAuction, Token } from "../../../generated/schema" import { ERC20Pool } from '../../../generated/templates/ERC20Pool/ERC20Pool' @@ -262,7 +262,7 @@ export function updatePool(pool: Pool): void { pool.quoteTokenBalance = wadToDecimal(unnormalizedTokenBalance.times(scaleFactor)) // update collateral token balances // use the appropriate contract for querying balanceOf the pool - if (pool.poolType == 'Fungible') { + if (pool.poolType === 'Fungible') { token = Token.load(pool.collateralToken)! scaleFactor = TEN_BI.pow(18 - token.decimals as u8) unnormalizedTokenBalance = getTokenBalance(Address.fromBytes(pool.collateralToken), poolAddress) diff --git a/src/utils/position.ts b/src/utils/position.ts index 773d80c..1665c8e 100644 --- a/src/utils/position.ts +++ b/src/utils/position.ts @@ -1,12 +1,16 @@ import { Address, BigInt, Bytes, dataSource, store } from "@graphprotocol/graph-ts" -import { Position, Token } from "../../generated/schema" -import { ONE_BI, ZERO_BI, positionManagerAddressTable } from "../utils/constants" +import { Bucket, Position, PositionLend, Token } from "../../generated/schema" +import { ONE_BI, ZERO_BD, ZERO_BI, positionManagerAddressTable } from "../utils/constants" import { addressToBytes } from "../utils/convert" -import { getTokenName, getTokenSymbol } from "./token-erc721" +import { getTokenName, getTokenSymbol, getTokenURI } from "./token-erc721" import { PositionManager } from "../../generated/PositionManager/PositionManager" import { bigIntToBytes } from "../utils/convert" +/*****************************/ +/*** Constructor Functions ***/ +/*****************************/ + export function loadOrCreateLPToken(tokenAddress: Address): Token { const id = addressToBytes(tokenAddress) let token = Token.load(id) @@ -27,6 +31,7 @@ export function loadOrCreateLPToken(tokenAddress: Address): Token { export function loadOrCreatePosition(tokenId: BigInt): Position { const byteTokenId = bigIntToBytes(tokenId) + const positionManagerAddress = positionManagerAddressTable.get(dataSource.network())! let position = Position.load(byteTokenId) if (position == null) { position = new Position(byteTokenId) as Position @@ -34,17 +39,107 @@ export function loadOrCreatePosition(tokenId: BigInt): Position { position.indexes = [] position.owner = Bytes.empty() position.pool = Bytes.empty() - position.token = Bytes.empty() + position.token = addressToBytes(positionManagerAddress) + position.tokenURI = getTokenURI(positionManagerAddress, tokenId) } return position } +export function getPositionLendId(tokenId: BigInt, bucketIndex: BigInt): Bytes { + return bigIntToBytes(tokenId).concat(bigIntToBytes(bucketIndex)) +} + +export function loadOrCreatePositionLend(tokenId: BigInt, bucketId: Bytes, bucketIndex: u32): PositionLend { + const positionLendId = getPositionLendId(tokenId, BigInt.fromI32(bucketIndex)) + let positionLend = PositionLend.load(positionLendId) + if (positionLend == null) { + positionLend = new PositionLend(positionLendId) as PositionLend + positionLend.bucket = bucketId + positionLend.bucketIndex = bucketIndex + positionLend.lpb = ZERO_BD + positionLend.lpbValueInQuote = ZERO_BD + positionLend.tokenId = tokenId + } + return positionLend +} + export function deletePosition(tokenId: BigInt): void { store.remove('Position', bigIntToBytes(tokenId).toHexString()) } +export function updatePositionLends(positionLend: PositionLend): void { + // add positionLend to bucket array if necessary + const bucket = Bucket.load(positionLend.bucket) + if (bucket != null) { + const existingBucketIndex = bucket.positionLends.indexOf(positionLend.id) + if (existingBucketIndex != -1) { + bucket.positionLends = bucket.positionLends.concat([positionLend.id]) + } + } + + // add positionLend to position array if necessary + const position = Position.load(bigIntToBytes(positionLend.tokenId))! + const existingPositionIndex = position.indexes.indexOf(positionLend.id) + if (existingPositionIndex != -1) { + position.indexes = position.indexes.concat([positionLend.id]) + } +} + +// if necessary +// remove association between PositionLend and Position and Buckets +// remove from store +export function saveOrRemovePositionLend(positionLend: PositionLend): void { + if (positionLend.lpb.equals(ZERO_BD)) { + // remove positionLend from bucket array + const bucket = Bucket.load(positionLend.bucket)! + const existingBucketIndex = bucket.positionLends.indexOf(positionLend.id) + const bucketPositionLends = bucket.positionLends + if (existingBucketIndex != -1) { + bucketPositionLends.splice(existingBucketIndex, 1) + } + bucket.positionLends = bucketPositionLends + + // remove positionLend from account array + const position = Position.load(bigIntToBytes(positionLend.tokenId))! + const existingPositionIndex = position.indexes.indexOf(positionLend.id) + const positionIndexes = position.indexes + if (existingPositionIndex != -1) { + positionIndexes.splice(existingPositionIndex, 1) + } + position.indexes = positionIndexes + + // remove positionLend from store + store.remove('PositionLend', positionLend.id.toHexString()) + } else { + positionLend.save() + } +} + +/*******************************/ +/*** Contract Call Functions ***/ +/*******************************/ + export function getPoolForToken(tokenId: BigInt): Address { const positionManagerAddress = positionManagerAddressTable.get(dataSource.network())! const positionManagerContract = PositionManager.bind(positionManagerAddress); return positionManagerContract.poolKey(tokenId) -} \ No newline at end of file +} + +export class PositionInfo { + lpb: BigInt + depositTime: BigInt + constructor(lpb: BigInt, depositTime: BigInt) { + this.lpb = lpb + this.depositTime = depositTime + } +} +export function getPositionInfo(tokenId: BigInt, bucketIndex: BigInt): PositionInfo { + const positionManagerAddress = positionManagerAddressTable.get(dataSource.network())! + const positionManagerContract = PositionManager.bind(positionManagerAddress); + const positionInfoResult = positionManagerContract.getPositionInfo(tokenId, bucketIndex) + + return new PositionInfo( + positionInfoResult.value0, + positionInfoResult.value1 + ) +} diff --git a/tests/position-manager.test.ts b/tests/position-manager.test.ts index 1ac4e5d..1516348 100644 --- a/tests/position-manager.test.ts +++ b/tests/position-manager.test.ts @@ -9,16 +9,17 @@ import { logStore, beforeEach } from "matchstick-as/assembly/index" -import { Address, BigInt, Bytes } from "@graphprotocol/graph-ts" +import { Address, BigInt, Bytes, dataSource } from "@graphprotocol/graph-ts" import { handleApproval, handleBurn, handleMemorializePosition, handleMint, handleMoveLiquidity, handleRedeemPosition } from "../src/mappings/position-manager" -import { assertPosition, createApprovalEvent, createBurnEvent, createMemorializePositionEvent, createMintEvent, createMoveLiquidityEvent, createRedeemPositionEvent, mintPosition } from "./utils/position-manager-utils" +import { assertPosition, assertPositionLend, createApprovalEvent, createBurnEvent, createMemorializePositionEvent, createMintEvent, createMoveLiquidityEvent, createRedeemPositionEvent, mintPosition } from "./utils/position-manager-utils" import { bigIntToBytes, wadToDecimal } from "../src/utils/convert" -import { create721Pool } from "./utils/common" -import { mockGetLPBValueInQuote, mockGetLenderInfo, mockGetPoolKey, mockGetTokenName, mockGetTokenSymbol } from "./utils/mock-contract-calls" +import { create721Pool, createAndHandleAddQuoteTokenEvent } from "./utils/common" +import { mockGetLPBValueInQuote, mockGetLenderInfo, mockGetPoolKey, mockGetPositionInfo } from "./utils/mock-contract-calls" import { Lend } from "../generated/schema" import { getLendId } from "../src/utils/pool/lend" import { getBucketId } from "../src/utils/pool/bucket" -import { FIVE_PERCENT_BI, ZERO_BI } from "../src/utils/constants" +import { FIVE_PERCENT_BI, TWO_BI, ZERO_BI, positionManagerAddressTable } from "../src/utils/constants" +import { getPositionLendId } from "../src/utils/position" // Tests structure (matchstick-as >=0.5.0) // https://thegraph.com/docs/en/developer/matchstick/#tests-structure-0-5-0 @@ -99,6 +100,7 @@ describe("Describe entity assertions", () => { // check position attributes assertPosition(lender, pool, tokenId, tokenContractAddress) + // check entity count after mint and before burn assert.entityCount("Burn", 0) assert.entityCount("Mint", 1) assert.entityCount("Position", 1) @@ -121,9 +123,11 @@ describe("Describe entity assertions", () => { `${tokenId}` ) + // check entity count after burn assert.entityCount("Burn", 1) assert.entityCount("Mint", 1) - // assert.entityCount("Position", 1) + assert.entityCount("Position", 0) + assert.entityCount("Token", 3) }) test("Mint", () => { @@ -141,8 +145,24 @@ describe("Describe entity assertions", () => { // check position attributes assertPosition(lender, pool, tokenId, tokenContractAddress) + // check mint attributes + assert.fieldEquals( + "Mint", + `0xa16081f360e3847006db660bae1c6d1b2e17ec2a01000000`, + "lender", + `${lender.toHexString()}` + ) + assert.fieldEquals( + "Mint", + `0xa16081f360e3847006db660bae1c6d1b2e17ec2a01000000`, + "tokenId", + `${tokenId}` + ) + + // check entity count assert.entityCount("Mint", 1) assert.entityCount("Position", 1) + assert.entityCount("Token", 3) }) test("Memorialize", () => { @@ -241,8 +261,6 @@ describe("Describe entity assertions", () => { assert.entityCount("RedeemPosition", 1) }) - // TODO: test handleTransferLP - // identified an issue with the duplicate recording of lend info on handleMoveLiquidity and handleTransferLP test("MoveLiquidity", () => { assert.entityCount("Mint", 0) assert.entityCount("MemorializePosition", 0) @@ -252,14 +270,30 @@ describe("Describe entity assertions", () => { const lender = Address.fromString("0x0000000000000000000000000000000000000020") const pool = Address.fromString("0x0000000000000000000000000000000000000001") const tokenId = BigInt.fromI32(234) - const tokenContractAddress = Address.fromString("0xa16081f360e3847006db660bae1c6d1b2e17ec2a") - const indexes:BigInt[] = [] + const tokenContractAddress = positionManagerAddressTable.get(dataSource.network())! + const indexes:BigInt[] = [BigInt.fromI32(5000), BigInt.fromI32(5500)] const fromIndex = BigInt.fromI32(5000) const toIndex = BigInt.fromI32(4000) const lpRedeemedFrom = BigInt.fromString("63380000000000000000") // 63.38 - const lpRedeemedto = BigInt.fromString("62740000000000000000") // 62.74 + const lpRedeemedTo = BigInt.fromString("62740000000000000000") // 62.74 const lpValueInQuote = BigInt.fromString("64380000000000000000") const expectedDepositTime = BigInt.fromI32(1000) + const lup = BigInt.fromString("9529276179422528643") // 9.529276179422528643 * 1e18 + const lpb = BigInt.fromString("630380000000000000000") // 630.38 + + /***********************/ + /*** Add Quote Token ***/ + /***********************/ + + for (let i = 0; i < indexes.length; i++) { + const index = indexes[i] + const amount = index.times(index) + const price = indexes[i].times(TWO_BI).toBigDecimal() + const logIndex = BigInt.fromI32(i) + + // create AddQuoteToken and Lend entities for each index + createAndHandleAddQuoteTokenEvent(pool, lender, index, price, amount, lpb, lup, logIndex) + } /*********************/ /*** Mint Position ***/ @@ -270,57 +304,66 @@ describe("Describe entity assertions", () => { // check position attributes assertPosition(lender, pool, tokenId, tokenContractAddress) + assert.entityCount("Account", 1) /****************************/ /*** Memorialize Position ***/ /****************************/ - const bucketId = getBucketId(pool, fromIndex.toU32()) - const lend = new Lend(getLendId(bucketId, lender)) - lend.bucket = bucketId - lend.bucketIndex = fromIndex.toU32() - lend.depositTime = BigInt.fromI32(1000) - lend.lender = lender - lend.pool = pool - lend.poolAddress = pool.toHexString() - lend.lpb = wadToDecimal(lpRedeemedFrom) - lend.lpbValueInQuote = wadToDecimal(lpValueInQuote) - lend.save(); - + // mock contract calls mockGetPoolKey(tokenId, pool) + for (let i = 0; i < indexes.length; i++) { + const index = indexes[i] + mockGetPositionInfo(tokenId, index, expectedDepositTime, lpb) + mockGetLPBValueInQuote(pool, lpb, index, lpValueInQuote) + } + // memorialize existing position const newMemorializeEvent = createMemorializePositionEvent(lender, tokenId, indexes) handleMemorializePosition(newMemorializeEvent) // check position attributes assertPosition(lender, pool, tokenId, tokenContractAddress) - // TODO: check index attributes + assert.entityCount("Account", 1) assert.entityCount("Mint", 1) - assert.entityCount("Lend", 1) + assert.entityCount("Lend", 2) assert.entityCount("MemorializePosition", 1) assert.entityCount("Position", 1) + assert.entityCount("PositionLend", 2) assert.entityCount("MoveLiquidity", 0) /**********************/ /*** Move Liquidity ***/ /**********************/ - mockGetLenderInfo(pool, toIndex, lender, lpRedeemedto, expectedDepositTime) - - mockGetLPBValueInQuote(pool, lpRedeemedFrom, fromIndex, lpValueInQuote) - mockGetLPBValueInQuote(pool, ZERO_BI, toIndex, ZERO_BI) - mockGetLPBValueInQuote(pool, lpRedeemedto, toIndex, lpValueInQuote) - const newMoveLiquidityEvent = createMoveLiquidityEvent(lender, tokenId, fromIndex, toIndex, lpRedeemedFrom, lpRedeemedto) + // mock contract calls + mockGetPositionInfo(tokenId, toIndex, expectedDepositTime, lpRedeemedTo) + mockGetLPBValueInQuote(pool, lpRedeemedTo, toIndex, lpValueInQuote) + mockGetLPBValueInQuote(pool, lpRedeemedTo, toIndex, lpValueInQuote) + mockGetLPBValueInQuote(pool, lpb.minus(lpRedeemedFrom), fromIndex, lpValueInQuote) + mockGetLPBValueInQuote(pool, lpb.minus(lpRedeemedFrom), fromIndex, lpValueInQuote) + + const newMoveLiquidityEvent = createMoveLiquidityEvent(lender, tokenId, fromIndex, toIndex, lpRedeemedFrom, lpRedeemedTo) handleMoveLiquidity(newMoveLiquidityEvent) + /********************/ + /*** Assert State ***/ + /********************/ + // check position attributes assertPosition(lender, pool, tokenId, tokenContractAddress) - // TODO: check index attributes + // check index attributes + assertPositionLend(getPositionLendId(tokenId, indexes[1]).toHexString(), getBucketId(pool, indexes[1].toU32()).toHexString(), expectedDepositTime, lpb) + assertPositionLend(getPositionLendId(tokenId, fromIndex).toHexString(), getBucketId(pool, fromIndex.toU32()).toHexString(), expectedDepositTime, lpb.minus(lpRedeemedFrom)) + assertPositionLend(getPositionLendId(tokenId, toIndex).toHexString(), getBucketId(pool, toIndex.toU32()).toHexString(), expectedDepositTime, lpRedeemedTo) + + assert.entityCount("Account", 1) assert.entityCount("Mint", 1) assert.entityCount("MemorializePosition", 1) assert.entityCount("Position", 1) + assert.entityCount("PositionLend", 3) assert.entityCount("MoveLiquidity", 1) }) diff --git a/tests/utils/mock-contract-calls.ts b/tests/utils/mock-contract-calls.ts index f55fefa..9772fc5 100644 --- a/tests/utils/mock-contract-calls.ts +++ b/tests/utils/mock-contract-calls.ts @@ -7,6 +7,8 @@ import { BurnInfo, DebtInfo, LoansInfo, PoolPricesInfo, PoolUtilizationInfo, Res import { AuctionInfo, AuctionStatus } from "../../src/utils/pool/liquidation" import { BorrowerInfo } from "../../src/utils/pool/loan" import { wdiv, wmin, wmul } from "../../src/utils/math" +import { addressToBytes, decimalToWad } from "../../src/utils/convert" +import { Pool } from "../../generated/schema" /*********************************/ @@ -102,6 +104,34 @@ export function mockGetPoolKey(tokenId: BigInt, expectedPoolAddress: Address): v ]) } +export function mockGetTokenURI(tokenId: BigInt, expectedTokenURI: String): void { + createMockedFunction( + positionManagerAddressTable.get(dataSource.network())!, + 'tokenURI', + 'tokenURI(uint256):(string)' + ) + .withArgs([ethereum.Value.fromUnsignedBigInt(tokenId)]) + .returns([ + ethereum.Value.fromString(expectedTokenURI), + ]) +} + +export function mockGetPositionInfo(tokenId: BigInt, bucketIndex: BigInt, expectedDepositTime: BigInt, expectedLPB: BigInt): void { + createMockedFunction( + positionManagerAddressTable.get(dataSource.network())!, + 'getPositionInfo', + 'getPositionInfo(uint256,uint256):(uint256,uint256)' + ) + .withArgs([ + ethereum.Value.fromUnsignedBigInt(tokenId), + ethereum.Value.fromUnsignedBigInt(bucketIndex) + ]) + .returns([ + ethereum.Value.fromUnsignedBigInt(expectedLPB), + ethereum.Value.fromUnsignedBigInt(expectedDepositTime), + ]) +} + /***************************/ /*** Pool Mock Functions ***/ /***************************/ @@ -343,6 +373,9 @@ export class PoolMockParams { collateralization: BigInt actualUtilization: BigInt targetUtilization: BigInt + // collateralTokenBalance: BigInt + // quoteTokenBalance: BigInt + // TODO: add quoteBalance and collateralBalance } // mock all pool poolInfoUtilis contract calls export function mockPoolInfoUtilsPoolUpdateCalls(pool: Address, params: PoolMockParams): void { @@ -387,6 +420,13 @@ export function mockPoolInfoUtilsPoolUpdateCalls(pool: Address, params: PoolMock ) mockGetPoolUtilizationInfo(pool, expectedPoolUtilizationInfo) + // TODO: pass expected balance to mock balance calls + // load pool instance + const poolInstance = Pool.load(addressToBytes(pool))! + // mock token balance calls + mockTokenBalance(Address.fromBytes(poolInstance.collateralToken), pool, decimalToWad(poolInstance.collateralBalance)) + mockTokenBalance(Address.fromBytes(poolInstance.quoteToken), pool, decimalToWad(poolInstance.quoteTokenBalance)) + mockGetRatesAndFees( pool, BigInt.fromString("850000000000000000"), // 0.85 * 1e18 diff --git a/tests/utils/position-manager-utils.ts b/tests/utils/position-manager-utils.ts index 7db4e04..6e7d334 100644 --- a/tests/utils/position-manager-utils.ts +++ b/tests/utils/position-manager-utils.ts @@ -10,9 +10,9 @@ import { RedeemPosition, Transfer } from "../../generated/PositionManager/PositionManager" -import { mockGetPoolKey, mockGetTokenName, mockGetTokenSymbol } from "./mock-contract-calls" +import { mockGetPoolKey, mockGetTokenName, mockGetTokenSymbol, mockGetTokenURI } from "./mock-contract-calls" import { handleMint } from "../../src/mappings/position-manager" -import { bigIntToBytes } from "../../src/utils/convert" +import { bigIntToBytes, wadToDecimal } from "../../src/utils/convert" export function createApprovalEvent( owner: Address, @@ -108,6 +108,7 @@ export function createMemorializePositionEvent( } export function createMintEvent( + positionManagerAddress: Address, lender: Address, pool: Address, tokenId: BigInt @@ -126,6 +127,8 @@ export function createMintEvent( new ethereum.EventParam("tokenId", ethereum.Value.fromUnsignedBigInt(tokenId)) ) + mintEvent.address = positionManagerAddress + return mintEvent } @@ -220,8 +223,10 @@ export function mintPosition(lender: Address, pool: Address, tokenId: BigInt, to mockGetPoolKey(tokenId, pool) mockGetTokenName(tokenContractAddress, "unknown") mockGetTokenSymbol(tokenContractAddress, "N/A") + const expectedTokenURI = "EXPECTED TOKEN URI" + mockGetTokenURI(tokenId, expectedTokenURI) - const newMintEvent = createMintEvent(lender, pool, tokenId) + const newMintEvent = createMintEvent(tokenContractAddress, lender, pool, tokenId) handleMint(newMintEvent) } @@ -248,3 +253,24 @@ export function assertPosition(lender: Address, pool: Address, tokenId: BigInt, `${tokenContractAddress.toHexString()}` ) } + +export function assertPositionLend(positionLendId: string, bucketId: string, expectedDepositTime: BigInt, lpb: BigInt): void { + assert.fieldEquals( + "PositionLend", + `${positionLendId}`, + "bucket", + `${bucketId}` + ) + assert.fieldEquals( + "PositionLend", + `${positionLendId}`, + "depositTime", + `${expectedDepositTime}` + ) + assert.fieldEquals( + "PositionLend", + `${positionLendId}`, + "lpb", + `${wadToDecimal(lpb)}` + ) +}