Add contracts
To add a new contract to your app, use the contracts
field in ponder.config.ts
. Ponder fetches raw blockchain data (event logs) for each contract you add, and passes that data to the indexing functions you write.
This guide explains how each contract configuration field works, and suggests patterns for common use cases. See also the ponder.config.ts
API reference page.
Contract name
Every contract must have a unique name, provided as a key to the contracts
object.
import { createConfig } from "@ponder/core";
import { http } from "viem";
import { BlitmapAbi } from "./abis/Blitmap";
export default createConfig({
networks: {
mainnet: { chainId: 1, transport: http(process.env.PONDER_RPC_URL_1) },
},
contracts: {
Blitmap: {
abi: BlitmapAbi,
network: "mainnet",
address: "0x8d04a8c79cEB0889Bdd12acdF3Fa9D207eD3Ff63",
startBlock: 12439123,
},
},
});
ABI
Each contract must have an ABI.
Ponder uses ABIType to offer autocomplete and strict type checking. This means ABIs must be saved in .ts
files and asserted as constant using as const
. For more information, please reference the ABIType documentation (opens in a new tab).
export const BlitmapAbi = [
{ inputs: [], stateMutability: "nonpayable", type: "constructor" },
{
inputs: [{ internalType: "address", name: "owner", type: "address" }],
name: "balanceOf",
outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
stateMutability: "view",
type: "function",
},
// ...
] as const;
import { createConfig } from "@ponder/core";
import { BlitmapAbi } from "./abis/Blitmap";
export default createConfig({
// ...
contracts: {
Blitmap: {
abi: BlitmapAbi,
network: "mainnet",
address: "0x8d04a8c79cEB0889Bdd12acdF3Fa9D207eD3Ff63",
startBlock: 12439123,
},
},
});
Multiple ABIs
It's occasionally useful to provide multiple ABIs for one contract, like when defining a proxy/upgradable contract that has gone through multiple implementation contracts. The mergeAbis
function exported by @ponder/core
safely removes duplicate ABI items and maintains strict types.
import { createConfig, mergeAbis } from "@ponder/core";
import { http } from "viem";
import { ERC1967ProxyAbi } from "./abis/ERC1967Proxy";
import { NameRegistryAbi } from "./abis/NameRegistry";
import { NameRegistry2Abi } from "./abis/NameRegistry2";
export default createConfig({
networks: {
goerli: { chainId: 5, transport: http(process.env.PONDER_RPC_URL_5) },
},
contracts: {
FarcasterNameRegistry: {
abi: mergeAbis([ERC1967ProxyAbi, NameRegistryAbi, NameRegistry2Abi]),
network: "goerli",
address: "0xe3Be01D99bAa8dB9905b33a3cA391238234B79D1",
startBlock: 7648795,
},
},
});
Network
Single network
If the contract is only deployed to one network, just pass the network name as a string to the network
field.
import { createConfig } from "@ponder/core";
import { http } from "viem";
import { BlitmapAbi } from "./abis/Blitmap";
export default createConfig({
networks: {
mainnet: {
chainId: 1,
transport: http(process.env.PONDER_RPC_URL_1),
},
},
contracts: {
Blitmap: {
abi: BlitmapAbi,
network: "mainnet",
address: "0x8d04...D3Ff63",
startBlock: 12439123,
},
},
});
Multiple networks
If you'd like to index the same contract (having the same ABI) across multiple networks, pass an object to the network
field containing network-specific options.
import { createConfig } from "@ponder/core";
import { http } from "viem";
import { UniswapV3FactoryAbi } from "./abis/UniswapV3Factory";
export default createConfig({
networks: {
mainnet: { chainId: 1, transport: http(process.env.PONDER_RPC_URL_1) },
base: { chainId: 8453, transport: http(process.env.PONDER_RPC_URL_8453) },
},
contracts: {
UniswapV3Factory: {
abi: UniswapV3FactoryAbi,
network: {
mainnet: {
address: "0x1F98431c8aD98523631AE4a59f267346ea31F984",
startBlock: 12369621,
},
base: {
address: "0x33128a8fC17869897dcE68Ed026d694621f6FDfD",
startBlock: 1371680,
},
},
},
},
});
Now, the indexing functions you write for UniswapV3Factory
will process events from both mainnet and Base.
The event
and context
objects are still strictly typed according to the configuration you provide. The context.network
object contains information about which network the current event is from.
ponder.on("UniswapV3Factory:Ownership", async ({ event, context }) => {
context.network;
// ^? { name: "mainnet", chainId 1 } | { name: "base", chainId 8453 }
event.log.address;
// ^? "0x1F98431c8aD98523631AE4a59f267346ea31F984" | "0x33128a8fC17869897dcE68Ed026d694621f6FDfD"
if (context.network.name === "mainnet") {
// Do mainnet-specific stuff!
}
});
Network override logic
Network-specific configuration uses an override pattern. Any options defined at the top level are the default, and the network-specific objects override those defaults. All fields other than abi
can be specified on a per-network basis, including address
, factory
, filter
, startBlock
, and endBlock
.
For example, the Uniswap V3 factory contract is deployed to the same address on most chains, but has a different address on Base.
import { createConfig } from "@ponder/core";
import { http } from "viem";
import { UniswapV3FactoryAbi } from "./abis/EntryPoint";
export default createConfig({
networks: {
mainnet: { chainId: 1, transport: http(process.env.PONDER_RPC_URL_1) },
optimism: { chainId: 10, transport: http(process.env.PONDER_RPC_URL_10) },
base: { chainId: 8453, transport: http(process.env.PONDER_RPC_URL_8453) },
},
contracts: {
UniswapV3Factory: {
abi: UniswapV3FactoryAbi,
address: "0x1F98431c8aD98523631AE4a59f267346ea31F984",
network: {
// No network-specific address provided for mainnet and Optimism.
// The default address will be used ("0x1F98431c8aD98523631AE4a59f267346ea31F984").
mainnet: { startBlock: 12369621 },
optimism: { startBlock: 0 },
// The network-specific address will be used ("0x33128a8fC17869897dcE68Ed026d694621f6FDfD").
base: {
address: "0x33128a8fC17869897dcE68Ed026d694621f6FDfD",
startBlock: 1371680,
},
},
},
},
});
On the other hand, the ERC-4337 Entry Point contract is deployed to the same address on all networks, so you could define the address
field at the top level.
import { createConfig } from "@ponder/core";
import { http } from "viem";
import { EntryPointAbi } from "./abis/EntryPoint";
export default createConfig({
networks: {
mainnet: { chainId: 1, transport: http(process.env.PONDER_RPC_URL_1) },
optimism: { chainId: 10, transport: http(process.env.PONDER_RPC_URL_10) },
},
contracts: {
EntryPoint: {
abi: EntryPointAbi,
address: "0x1F98431c8aD98523631AE4a59f267346ea31F984",
network: {
mainnet: { startBlock: 12369621 },
optimism: { startBlock: 88234528 },
},
},
},
});
Address
Single address
The simplest (and most common) option is to pass a single static address.
import { createConfig } from "@ponder/core";
import { http } from "viem";
import { BlitmapAbi } from "./abis/Blitmap";
export default createConfig({
networks: {
mainnet: { chainId: 1, transport: http(process.env.PONDER_RPC_URL_1) },
},
contracts: {
Blitmap: {
abi: BlitmapAbi,
network: "mainnet",
address: "0x8d04a8c79cEB0889Bdd12acdF3Fa9D207eD3Ff63",
startBlock: 12439123,
},
},
});
Multiple addresses
The address
field also accepts a list of contract addresses.
This option can be used to index multiple contracts with known addresses that have the same ABI (or share an interface, like ERC721) using the same indexing functions.
When using this option, startBlock
is shared across all addresses. It's
often best to use the earliest deployment block among them.
import { createConfig } from "@ponder/core";
import { http } from "viem";
import { ERC721Abi } from "./abis/ERC721";
export default createConfig({
networks: {
mainnet: { chainId: 1, transport: http(process.env.PONDER_RPC_URL_1) },
},
contracts: {
NiceJpegs: {
abi: ERC721Abi,
network: "mainnet",
address: [
"0x4E1f41613c9084FdB9E34E11fAE9412427480e56", // Terraforms
"0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D", // BAYC
"0x8a90CAb2b38dba80c64b7734e58Ee1dB38B8992e", // Doodles
"0x0000000000664ceffed39244a8312bD895470803", // !fundrop
],
startBlock: 12439123,
},
},
});
Factory contracts
The factory
field specifies a set of contracts that are created by a factory. The factory
and address
fields are mutually exclusive.
import { createConfig } from "@ponder/core";
import { parseAbiItem } from "viem";
export default createConfig({
networks: {
mainnet: { chainId: 1, transport: http(process.env.PONDER_RPC_URL_1) },
},
contracts: {
SudoswapPool: {
abi: SudoswapPoolAbi,
network: "mainnet",
factory: {
// The address of the factory contract that creates instances of this child contract.
address: "0xb16c1342E617A5B6E4b631EB114483FDB289c0A4",
// The event emitted by the factory that announces a new instance of this child contract.
event: parseAbiItem("event NewPair(address poolAddress)"),
// The name of the parameter that contains the address of the new child contract.
parameter: "poolAddress",
},
startBlock: 14645816,
},
},
});
Now, the indexing functions you write for SudoswapPool
will process events emitted by all child contracts that are created by the specified factory. The event.log.address
field contains the address of the child contract that emitted the current event.
import { ponder } from "@/generated";
ponder.on("SudoswapPool:Transfer", async ({ event }) => {
// This is the address of the child contract that emitted the event.
event.log.address;
// ^? string
});
Factory contract requirements & limitations
- Scaling: Ponder does not currently support factory contracts with more than 10,000 children. This includes Uniswap V2, V3, and many other permissionless pool-based DeFi protocols. Why not? The Ethereum JSON-RPC API does not scale well for factory contracts. There's only so much that Ponder's sync engine can do about this while remaining compatible with the JSON-RPC API. We're actively working to address scaling challenges via features like remote sync, shared caching, and concurrent indexing. Thanks for your patience.
- Event signature requirements: The factory contract must emit an event log announcing the creation of each new child contract that contains the new child contract address as a named parameter (with type
"address"
). The parameter can be either indexed or non-indexed. Here are a few factory event signatures with their eligibility explained:
// ✅ Eligible. The parameter "child" has type "address" and is non-indexed.
event ChildContractCreated(address child);
// ✅ Eligible. The parameter "pool" has type "address" and is indexed.
event PoolCreated(address indexed deployer, address indexed pool, uint256 fee);
// ❌ Ineligible. The parameter "contracts" is an array type, which is not supported.
// Always emit a separate event for each child contract, even if they are created in a batch.
event ContractsCreated(address[] contracts);
// ❌ Ineligible. The parameter "child" is a struct/tuple, which is not supported.
struct ChildContract {
address addr;
}
event ChildCreated(ChildContract child);
- Nested factory patterns: Ponder doesn't support factory patterns that are nested beyond a single layer.
Proxy & upgradable contracts
To index a proxy/upgradable contract, use the proxy contract address in the address
field. Then, be sure to include the ABIs of all implementation contracts that the proxy has ever had. The implementation ABIs are required to properly identify and decode all historical event logs. To add multiple ABIs safely, use the mergeAbis
utility function.
Tip: On Etherscan, there is a link to the current implementation contract on the Contract → Read as Proxy tab. You can copy all the implementation ABIs as text and paste them into .ts
files.
Event filter
Custom event filters allow you to index events emitted by any contract on the network that match some criteria.
By event name or signature
The filter.event
field accepts an event name (or list of event names) present in the provided ABI.
import { createConfig } from "@ponder/core";
import { http } from "viem";
import { ERC20Abi } from "./abis/ERC20";
export default createConfig({
networks: {
mainnet: { chainId: 1, transport: http(process.env.PONDER_RPC_URL_1) },
},
contracts: {
ERC20: {
abi: ERC20Abi,
network: "mainnet",
filter: { event: "Transfer" },
// ^? "Transfer" | "Approval" | ("Transfer" | "Approval")[]
startBlock: 18500000,
endBlock: 18505000,
},
},
});
The indexing functions you write will run for all events matching the filter, regardless of which contract emitted them.
import { ponder } from "@/generated";
ponder.on("ERC20:Transfer", async ({ event }) => {
// This is the address of the contract that emitted the event.
event.log.address;
// ^? string
});
By indexed argument value
You can use the filter.event
and filter.args
options together to filter for events that have specific indexed argument values. Each indexed argument field accepts a single value or a list of values to match.
This example filters for all ERC20 Transfer
events where the from
parameter equals a specific address, and the to
parameter equals one of two addresses.
import { createConfig } from "@ponder/core";
import { http } from "viem";
import { ERC20Abi } from "./abis/ERC20";
export default createConfig({
networks: {
mainnet: { chainId: 1, transport: http(process.env.PONDER_RPC_URL_1) },
},
contracts: {
ERC20: {
abi: ERC20Abi,
network: "mainnet",
filter: {
event: "Transfer",
args: {
from: "0xa0ee7a142d267c1f36714e4a8f75612f20a79720",
to: [
"0x06012c8cf97bead5deae237070f9587f8e7a266d",
"0x7c40c393dc0f283f318791d746d894ddd3693572",
],
},
},
startBlock: 18500000,
endBlock: 18505000,
},
},
});
The indexing function will run for all events matching the filter.
import { ponder } from "@/generated";
ponder.on("ERC20:Transfer", async ({ event }) => {
// This will always be "0xa0ee7a142d267c1f36714e4a8f75612f20a79720"
event.params.from;
// This will be one of the two addresses above
event.params.to;
});
Block range
The optional startBlock
and endBlock
options specify the block range to index. The default value for startBlock
is 0
. If you don't provide an endBlock
, Ponder will index the contract in realtime and handle chain reorganizations.
Yes, it's annoying to keep track of deployment block numbers. We're working on ways to automatically detect them.
import { createConfig } from "@ponder/core";
import { http } from "viem";
import { BlitmapAbi } from "./abis/Blitmap";
export default createConfig({
networks: {
mainnet: { chainId: 1, transport: http(process.env.PONDER_RPC_URL_1) },
},
contracts: {
Blitmap: {
abi: BlitmapAbi,
network: "mainnet",
address: "0x8d04a8c79cEB0889Bdd12acdF3Fa9D207eD3Ff63",
startBlock: 12439123,
endBlock: 16500000,
},
},
});