Developers Docs
Search
⌃K

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.

Invest in market

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 argument event
  • account : This is Account entity that is created for the user making this investment. We will need to create this entity by calling getOrCreateAccount function of code library
  • market : 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 as event.address in this function
  • outputTokenAmount : This is amount of output token that is minted by market for user. It's not provided in the Mint event parameters so we need to get it from somewhere else. As discussed in Understanding Smart Contracts article, we can get it's value from Transfer event
  • inputTokenAmounts : These values are available as parameters in Mint event
  • rewardTokenAmounts : This will be an empty array because there is no reward given at the time of providing liquidity to a SushiSwap pair
  • outputTokenBalance : This is not available in the Mint event. We can calculate this value by fetching this user's existing position in this market and adding outputTokenAmount to Position.outputTokenBalance but we recommend that event handlers should not interact with the Position 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 entity AccountLiquidity
  • inputTokenBalances : This also require us to use Market.inputTokenBalances to compute new market reserves after adding newly deposited tokens. Then divide these new reserve values with Market.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 use Market entity values directly and instead define a new entity to track pair's reserves and total supply. We call this new entity pair
  • rewardTokenBalance : This will be an empty array
  • transferredFrom : 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 events
function 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
)
}
}

Transfer event handler

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.

Sync event handler

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.

Mint event handler

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.

Redeem from 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 .

Transfer of LP tokens

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.

Transfer to zero address

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.

Finished

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.