Skip to main content
Version: Firesquid

Frontier EVM support

This section describes additional options available for Substrate chains with the Frontier EVM pallet like Moonbeam or Astar. Follow the EVM squid tutorial for a step-by-step tutorial on building an EVM-processing. We recommend using squid-frontier-evm-template as a reference.

The page describes the additional options available for SubstrateBatchProcessor. The handler-based SubstrateProcessor exposes similar interfaces with the addXXXHandler methods. Please refer to the inline docs for details.

Subscribe to EVM events

Use addEvmLog(contract: string | string[], options) to subscribe to the EVM log data (event) emitted by a specific EVM contract:

const processor = new SubstrateBatchProcessor()
.setDataSource({
archive: lookupArchive("moonbeam", { release: "FireSquid" }),
})
.setTypesBundle("moonbeam")
.addEvmLog("0xb654611f84a8dc429ba3cb4fda9fad236c505a1a", {
filter: [erc721.events["Transfer(address,address,uint256)"].topic],
});

The option argument supports the same selectors as for addEvent and additionally a set of topic filters:

{
range?: DataRange,
filter?: EvmTopicSet[],
data?: {} // same as the data selector for `addEvent`
}

Note, that the topic filter follows the Ether.js filter specification. For example, for a filter that accepts the ERC721 topic Transfer(address,address,uint256) AND ApprovalForAll(address,address,bool) use a double array:

processor.addEvmLog('0xb654611f84a8dc429ba3cb4fda9fad236c505a1a', {
filter: [[
erc721.events["Transfer(address,address,uint256)"].topic,
erc721.events["ApprovalForAll(address,address,bool)"].topic
]]
})

Since @subsquid/substrate-processor@1.7.0 it is possible to pass multiple contracts to addEvmLog():

processor.addEvmLog([
'0xb654611f84a8dc429ba3cb4fda9fad236c505a1a',
'0x6a2d262D56735DbA19Dd70682B39F6bE9a931D98'
], {
topics: ['0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef']
})

Subscribe to EVM transactions

Since @subsquid/substrate-processor@1.7.0

It is possible to subscribe to Ethereum.transact() calls with the option to filter by the contract address (or addresses) and sighash, used as a function selector by the EVM spec. The data selection options are similar to addCall().

Note that by default both successful and failed transactions are fetched. Further, there's a difference between the success of a Substrate call and the internal EVM transaction, the transaction may fail even if the enclosing Substrate call has succeeded.

Examples

Request all EVM calls to the contract 0x6a2d262D56735DbA19Dd70682B39F6bE9a931D98:

processor.addEthereumTransaction('0x6a2d262D56735DbA19Dd70682B39F6bE9a931D98')

Request all EVM calls with the signature transfer(address,uint256):

processor.addEthereumTransaction('*', {sighash: '0xa9059cbb'})

Request the same data from multiple contracts at once:

processor.addEthereumTransaction([
'0x6a2d262D56735DbA19Dd70682B39F6bE9a931D98',
'0x3795C36e7D12A8c252A20C5a7B455f7c57b60283'
], {
sighash: '0xa9059cbb'
})

Typegen

squid-evm-typegen is used to generate type-safe facade classes to call the contract state and decode the log events. By convention, the generated classes and the ABI file is kept in src/abi.

npx squid-evm-typegen --abi=src/abi/ERC721.json --output=src/abi/erc721.ts

The file generated by squid-evm-typegen defines the events object with methods for decoding EVM logs into a typed object:

src/abi/erc721.ts
export const events = {
// for each topic defined in the ABI
"Transfer(address,address,uint256)": {
topic: abi.getEventTopic("Transfer(address,address,uint256)"),
decode(data: EvmLog): Transfer0Event {
return decodeEvent("Transfer(address,address,uint256)", data)
}
}
//...
}

It can be the used in the handler in the following way:

for (const block of ctx.blocks) {
for (const item of block.items) {
if (item.name === "EVM.Log") {
const { from, to, tokenId } = erc721.events["Transfer(address,address,uint256)"].decode(item.event.args)
}
}
}

Access the contract state

The EVM contract state is accessed using the generated Contract class that takes the handler context and the contract address as constructor arguments. The state is always accessed at the context block height unless explicitly defined in the constructor.

src/abi/erc721.ts
export class Contract  {
constructor(ctx: BlockContext, address: string)
constructor(ctx: ChainContext, block: Block, address: string) {
//...
}
private async call(name: string, args: any[]) : Promise<ReadonlyArray<any>> {
//...
}
async balanceOf(owner: string): Promise<ethers.BigNumber> {
return this.call("balanceOf", [owner])
}
}

It then can be constructed using the context variable and queried in a straightforward way (see squid-frontier-evm-template for a full example):

// ...
const CONTRACT_ADDRESS= "0xb654611f84a8dc429ba3cb4fda9fad236c505a1a"

processor.run(new TypeormDatabase(), async ctx => {
for (const block of ctx.blocks) {
for (const item of block.items) {
if (item.name === "EVM.Log") {
const contract = new erc721.Contract(ctx, block, CONTRACT_ADDRESS);
// query the contract state
const uri = await contract.tokenURI(1137)
}
}
}
})

For more information on EVM Typegen, see this dedicated page.

Event and transaction data

The way the Frontier EVM pallet exposes EVM logs and transaction may change due to runtime upgrades. The util library @subsquid/substrate-frontier-evm provides helper classes that are aware of the upgrades:

getEvmLog(ctx: ChainContext, event: Event): EvmLog

Extract the EVM log data from EVM.Log event.

getTransaction(ctx: ChainContext, call: Call): LegacyTransaction | EIP2930Transaction | EIP1559Transaction

Extract the transaction data from Ethereum.transact call with additional fields depending on the EVM transaction type.

Example


const processor = new SubstrateBatchProcessor()
.setBatchSize(200)
.setDataSource({
archive: lookupArchive('moonbeam', { release: 'FireSquid' })
})
.addEthereumTransaction('*', {
data: {
call: true,
}
})
.addEvmLog('*', {
data: {
event: true
}
})


processor.run(new TypeormDatabase(), async ctx => {
for (const block of ctx.blocks) {
for (const item of block.items) {
if (item.kind === 'event' && item.name === 'EVM.Log') {
const { address, data, topics } = getEvmLog(ctx, item.event)
// process evm log data
}
if (item.kind === 'call' && item.name === 'Ethereum.transact') {
const tx = getTransaction(ctx, item.call)
}

}
}
})

Factory contracts

It some cases the set of contracts to be indexed by the squid is not known in advance. For example, a DEX contract typically creates a new contract for each trading pair added, and each such trading contract is of interest.

While the set of handler subscriptions is static and defined at the processor creation, one can leverage the wildcard subscriptions and filter the contract of interest in runtime.

Let's consider how it works in a DEX example, with a contract emitting 'PairCreated(address,address,address,uint256)' log when a new pair trading contract is created by the main contract. The full code (used by BeamSwap) is available in this repo.

const FACTORY_ADDRESS = '0x985bca32293a7a496300a48081947321177a86fd'
const PAIR_CREATE_TOPIC = abi.events['PairCreated(address,address,address,uint256)'].decode(evmLog)
// subscribe to events when a new contract is created by the parent
// factory contract
const processor = new SubstrateBatchProcessor()
.addEvmLog(FACTORY_ADDRESS, {
filter: [PAIR_CREATED_TOPIC],
})
// Subscribe to all contracts emitting the events of interest, and
// later filter by the addresses deployed by the factory
processor.addEvmLog('*', {
filter: [
[
pair.events['Transfer(address,address,uint256)'].topic,
pair.events['Sync(uint112,uint112)'].topic,
pair.events['Swap(address,uint256,uint256,uint256,uint256,address)'].topic,
pair.events['Mint(address,uint256,uint256)'].topic,
pair.events['Burn(address,uint256,uint256,address)'].topic,
],
],
})

processor.run(database, async (ctx) => {
const mappers: BaseMapper<any>[] = []

for (const block of ctx.blocks) {
for (const item of block.items) {
if (item.kind === 'event') {
if (item.name === 'EVM.Log') {
await handleEvmLog(ctx, block.header, item.event)
}
}
}
}
})

async function handleEvmLog(ctx: BatchContext<Store, unknown>, block: SubstrateBlock, event: EvmLogEvent) {
const evmLog = getEvmLog(ctx, event)
const contractAddress = evmLog.address
if (contractAddress === FACTORY_ADDRESS && evmLog.topics[0] === PAIR_CREATED_TOPIC) {
// updated the list of contracts to whatch
} else if (await isPairContract(ctx.store, contractAddress)) {
// the contract has been created by the factory,
// index the events
}
}