Implement Event Handlers
Now that we have developed part of the subgraph that takes care of creating
Market
entities and all subgraph sources to listen to market specific events, let's start implementation of mapping code to index user investments in this market.Our approach towards implementing subgraph will be to call
investInMarket
, redeemFromMarket
and updateMarket
function from SimpleFi mapping code library in event handlers for Mint
, Burn
and Sync
respectively. Let's discuss every case one by one.Below is initial code of handler function of
Mint
event// Mint(address indexed sender, uint amount0, uint amount1)
export function handleMint(event: Mint): void {
// We need to collect data arguments of this call
investInMarket(
event,
account,
market,
outputTokenAmount,
inputTokenAmounts,
rewardTokenAmounts,
outputTokenBalance,
inputTokenBalances,
rewardTokenBalances,
transferredFrom
)
}
Let's discuss each function argument and see how we can get the required data for it -
event
: It's as handler function argumentevent
account
: This isAccount
entity that is created for the user making this investment. We will need to create this entity by callinggetOrCreateAccount
function of code librarymarket
: We can fetch the market entity for this pair by loading it directly from it's ID. We are sure that the market entity exists because we create the market entity before starting the source which triggers this handler function. ID of the market is same as address of the pair contract which is available asevent.address
in this functionoutputTokenAmount
: This is amount of output token that is minted by market for user. It's not provided in theMint
event parameters so we need to get it from somewhere else. As discussed in Understanding Smart Contracts article, we can get it's value fromTransfer
eventinputTokenAmounts
: These values are available as parameters inMint
eventrewardTokenAmounts
: This will be an empty array because there is no reward given at the time of providing liquidity to a SushiSwap pairoutputTokenBalance
: This is not available in theMint
event. We can calculate this value by fetching this user's existing position in this market and addingoutputTokenAmount
toPosition.outputTokenBalance
but we recommend that event handlers should not interact with thePosition
entity directly as it can be tricky to get latest position of a user in a market because of internal structure of the entities and their relationships. We will instead define a new entity in schema to keep track of user's output token balance in a market. We call this new entityAccountLiquidity
inputTokenBalances
: This also require us to useMarket.inputTokenBalances
to compute new market reserves after adding newly deposited tokens. Then divide these new reserve values withMarket.outputTokenTotalSupply
to get the amount of share of the user in the market reserves. Again to avoid issues arising from intermingles updates we don't useMarket
entity values directly and instead define a new entity to track pair's reserves and total supply. We call this new entitypair
rewardTokenBalance
: This will be an empty arraytransferredFrom
: This will be null because user is providing the liquidity directly be depositing input tokens to the market
As discussed in Understanding Smart Contractswe need to implement handlers for
Transfer
, Sync
and Mint
events and collect data in a temporary entity to be able to create above required arguments for investInMarket
function. We add following two entities in schema.graphql
type AccountLiquidity @entity {
id: ID! # {pairAddress}{accoutnAddress}
pair: Pair!
account: Account!
balance: BigInt!
}
type Mint @entity {
id: ID! # {trasactionHash}
pair: Pair
to: Account
liquityAmount: BigInt
amount0: BigInt
amount1: BigInt
transferEventApplied: Boolean!
syncEventApplied: Boolean!
mintEventApplied: Boolean!
}
We then add following code to
uniswapV2Pair.ts
to handle relevant eventsfunction getOrCreateMint(event: ethereum.Event, pair: PairEntity): MintEntity {
let mint = MintEntity.load(event.transaction.hash.toHexString())
if (mint != null) {
return mint as MintEntity
}
mint = new MintEntity(event.transaction.hash.toHexString())
mint.pair = pair.id
mint.transferEventApplied = false
mint.syncEventApplied = false
mint.mintEventApplied = false
mint.save()
return mint as MintEntity
}
function getOrCreateLiquidity(pair: PairEntity, accountAddress: Address): AccountLiquidityEntity {
let id = pair.id.concat("-").concat(accountAddress.toHexString())
let liqudity = AccountLiquidityEntity.load(id)
if (liqudity != null) {
return liqudity as AccountLiquidityEntity
}
liqudity = new AccountLiquidityEntity(id)
liqudity.pair = pair.id
liqudity.account = getOrCreateAccount(accountAddress).id
liqudity.balance = BigInt.fromI32(0)
liqudity.save()
return liqudity as AccountLiquidityEntity
}
function createOrUpdatePositionOnMint(event: ethereum.Event, pair: PairEntity, mint: MintEntity): void {
let isComplete = mint.transferEventApplied && mint.syncEventApplied && mint.mintEventApplied
if (!isComplete) {
return
}
let accountAddress = Address.fromString(mint.to)
let account = new AccountEntity(mint.to)
let market = MarketEntity.load(mint.pair) as MarketEntity
let accountLiquidity = getOrCreateLiquidity(pair, accountAddress)
let outputTokenAmount = mint.liquityAmount as BigInt
let inputTokenAmounts: TokenBalance[] = []
inputTokenAmounts.push(new TokenBalance(pair.token0, mint.to, mint.amount0 as BigInt))
inputTokenAmounts.push(new TokenBalance(pair.token1, mint.to, mint.amount1 as BigInt))
let outputTokenBalance = accountLiquidity.balance
let token0Balance = outputTokenBalance.times(pair.reserve0).div(pair.totalSupply)
let token1Balance = outputTokenBalance.times(pair.reserve1).div(pair.totalSupply)
let inputTokenBalances: TokenBalance[] = []
inputTokenBalances.push(new TokenBalance(pair.token0, mint.to, token0Balance))
inputTokenBalances.push(new TokenBalance(pair.token1, mint.to, token1Balance))
investInMarket(
event,
account,
market,
outputTokenAmount,
inputTokenAmounts,
[],
outputTokenBalance,
inputTokenBalances,
[],
null
)
// update market
let marketInputTokenBalances: TokenBalance[] = []
marketInputTokenBalances.push(new TokenBalance(pair.token0, pair.id, pair.reserve0))
marketInputTokenBalances.push(new TokenBalance(pair.token1, pair.id, pair.reserve1))
// Update market
updateMarket(
event,
market,
marketInputTokenBalances,
pair.totalSupply
)
store.remove('Mint', mint.id)
}
export function handleTransfer(event: Transfer): void {
if (event.params.value == BigInt.fromI32(0)) {
return
}
let pairAddressHex = event.address.toHexString()
let fromHex = event.params.from.toHexString()
let toHex = event.params.to.toHexString()
let pair = PairEntity.load(pairAddressHex) as PairEntity
// update account balances
if (fromHex != ADDRESS_ZERO) {
let accountLiquidityFrom = getOrCreateLiquidity(pair, event.params.from)
accountLiquidityFrom.balance = accountLiquidityFrom.balance.minus(event.params.value)
accountLiquidityFrom.save()
}
if (fromHex != pairAddressHex) {
let accountLiquidityTo = getOrCreateLiquidity(pair, event.params.to)
accountLiquidityTo.balance = accountLiquidityTo.balance.plus(event.params.value)
accountLiquidityTo.save()
}
// Check if transfer it's a mint or burn or transfer transaction
// minting new LP tokens
if (fromHex == ADDRESS_ZERO) {
if (toHex == ADDRESS_ZERO) {
pair.totalSupply = pair.totalSupply.plus(event.params.value)
pair.save()
}
let mint = getOrCreateMint(event, pair)
mint.transferEventApplied = true
mint.to = getOrCreateAccount(event.params.to).id
mint.liquityAmount = event.params.value
mint.save()
createOrUpdatePositionOnMint(event, pair, mint)
}
// send to pair contract before burn method call
if (fromHex != ADDRESS_ZERO && toHex == pairAddressHex) {
}
// internal _burn method call
if (fromHex == pairAddressHex && toHex == ADDRESS_ZERO) {
}
// everything else
if (fromHex != ADDRESS_ZERO && fromHex != pairAddressHex && toHex != pairAddressHex) {
}
}
export function handleMint(event: Mint): void {
let pair = PairEntity.load(event.address.toHexString()) as PairEntity
let mint = getOrCreateMint(event, pair)
mint.mintEventApplied = true
mint.amount0 = event.params.amount0
mint.amount1 = event.params.amount1
mint.save()
createOrUpdatePositionOnMint(event, pair, mint)
}
export function handleSync(event: Sync): void {
let transactionHash = event.transaction.hash.toHexString()
let pair = PairEntity.load(event.address.toHexString()) as PairEntity
pair.reserve0 = event.params.reserve0
pair.reserve1 = event.params.reserve1
pair.save()
let isSyncOnly = true
let possibleMint = MintEntity.load(transactionHash)
if (possibleMint != null) {
isSyncOnly = false
let mint = possibleMint as MintEntity
mint.syncEventApplied = true
mint.save()
pair.totalSupply = pair.totalSupply.plus(mint.liquityAmount as BigInt)
pair.save()
createOrUpdatePositionOnMint(event, pair, mint)
}
if (isSyncOnly) {
let inputTokenBalances: TokenBalance[] = []
inputTokenBalances.push(new TokenBalance(pair.token0, pair.id, pair.reserve0))
inputTokenBalances.push(new TokenBalance(pair.token1, pair.id, pair.reserve1))
// Update market
let market = MarketEntity.load(event.address.toHexString()) as MarketEntity
updateMarket(
event,
market,
inputTokenBalances,
pair.totalSupply
)
}
}
It's pretty simple, we are simply checking if a transfer is part of a mint, burn or a user to user to transfer. In case it's a mint we increase balance of the
to
address in AccountLiquidity
entity. We also create a Mint
entity by calling getOrCreateMint
function. We created this helper function because we did not want to rely on ordering of event triggers in subgraph. So no matter which event handler is triggered first our code will create the Mint
entity only once and will keep populating data available in the event being processed.Though after discussion with The Graph team we found that we can trust the ordering of event triggers in subgraph, we still found it better to use this
getOrCreate
pattern as it avoids a lot of errors because of edge cases. This also makes it easy to debug in case something unexpected happens. We can simply check if the entity exists and which event handlers have been processed for the transaction for which this entity was created. This pattern become most helpful when we need to implement call handlers which are triggered after triggering all event handlers for a specific transaction. For example if we had to add a call handler for mint
method of UniswapV2Pair.sol
smart contract then this call handler function will be executed after all the event handlers for Transfer
, Sync
and Mint
have been executed. Now we may think intuitively that in the scope of these event handlers the Mint
entity should exist but it does not exist because call handlers was never executed before these event handlers. Therefore instead of assuming existence and loading we recommend this getOrCreate
pattern which will check of existence of the entity and then create if not existing or fetch if existing. Trust me when I say that this pattern has saved us a lot of hours of debugging.After creating
Mint
entity we simply populate to
and liquidity
amount attributes in Transfer
event handler as they are required in investInMarket
function.In this event handlers also we need to figure out if the event is part of a
Mint
, Burn
or Swap
transaction. We know that in case of Mint
there should have been a transfer event before it and there should either be a Mint
entity for the transaction. We check for existing Mint
entity and if it exists then we populate it.If
Mint
entity does not exists then we simply update the Pair
entity and call updateMarket
to update market reserves.In this event handler we don't need to check anything as it is emitted only in a
Mint
transaction. We fetch an existing Mint
entity and populate it's attributes amount0
and amount1
. This completes the Mint
entity and then calling createOrUpdatePositionOnMint
will find that all events have been applied and it will update the common entities Position to create or update an existing position in this market.Burn
transaction works similar to Mint
transaction and we need to use similar logic of populating a temporary Burn
entity to get the all the required arguments for redeemFromMarket
call. Code is straight forward and can be seen at UniswapV2Pair.ts
.In Sushi Swap one can transfer the LP tokens they get by depositing their ERC20 tokens to the pair contract. For our subgraph we need to track these transfers as well because on a transfer position of the both the accounts is changing. To track these changes in positions we also implement a
handleTransfer
function which listens on Transfer
event of ERC20
specification. This function calls redeemFromMarket
for the user sending LP tokens and calls investInMarket
for user receiving LP tokens. This way we keep track of latest balance of all the accounts while also storing information of how they got this balance - by investing or by transfer from someone.Almost all subgraphs need to handle this specific case we are about to discuss. The case is of transfer to zero address. It is a tricky case because a transfer to zero could be part of a redeem transaction and could be a manual (accidental) transfer by a user. In our subgraph we need to detect which case is it because in case of redeem we also need to update the market reserves and in case of manual transfer only user position changes but market reserves remain same.
In Sushi Swap we find that the transfer to zero is emitted before
Burn
event so while handling Transfer
event we can not figure out if it's part of a Burn
transaction or manual transfer. We will get to know about it only while processing next transaction because we can not call a function in subgraph at the end of a transaction to figure out if Burn
event was emitted or not. To handle this we store
lastTransferToZero
attribute in Pair
entity that we populate every time there is a transfer to zero address. Then in every handler we check if this attribute is not null. If it's not null then we check that the current transaction is same as the transaction for which lastTransferToZero
was populated. If it is then we don't do anything as we are not yet sure if it's manual transfer or burn transaction. In the Burn
event handler we set lastTransferToZero
to null because we don't need to process it as a manual transfer to zero. If current transaction being process is different than the lastTransferToZero
then we know that there was not Burn transaction for this transfer to zero transaction because if it was a burn transaction then lastTransferToZero
would have been null. So in the case when lastTransferToZero
is not null and is different than current transaction being process then it's a manual transfer to zero and then we process it as a redeemFromMarket
for the user sending the LP tokens.To understand the code around this you can look for
pair.lastTransferZero
and checkIncompleteBurnFromLastTransaction
calls in the subgraph code.This is it fellow developers, we have completed the Sushi Swap subgraph and are now able to track all the user positions in all the pairs of Sushi Swap protocol.
Thank you for taking interest in learning about SimpleFi Dashboard. Please join our discord to get a. We will keep working on this tutorial to add more details based on your feedback and our findings to make it a self sufficient guide to subgraph development for integration with SimpleFi Dashboad.
Last modified 1yr ago