Treat your ABI like any other versioned artifact. Most frontend-contract sync bugs are ABI lifecycle problems in disguise.
Ready-to-use tools in scripts/:
ScriptPurposesync-abi.shSync compiled ABIs from Foundry/Hardhat artifacts to frontendabi-diff.jsCompare two ABI files: find added/removed/changed, flag breaking changes
bash
# Set which contracts to sync: create .abi-sync in your project root
echo "MyToken" >> .abi-sync
echo "MyVault:Vault" >> .abi-sync # writes as Vault.json
# Run from your project root
ABI_SOURCE=out ABI_DEST=frontend/src/abis bash path/to/sync-abi.sh
# Or use defaults (Foundry: out/ → frontend/src/abis/)
bash scripts/sync-abi.sh
Handles both Foundry ( out/Foo.sol/Foo.json) and Hardhat ( artifacts/contracts/) artifacts. Uses jq if available, falls back to Python.
bash
node scripts/abi-diff.js old/MyToken.json new/MyToken.json
# → { added: [], removed: [], changed: [], breaking: false, summary: "0 added, 0 removed, 1 changed" }
# Exit code 1 if breaking changes (useful in CI):
node scripts/abi-diff.js prev.json current.json || echo "BREAKING CHANGE"
Accepts raw ABI arrays or Foundry/Hardhat artifacts (auto-detected).
See references/abi-formats.md for complete coverage of ABI entry types, function selectors, event topics, Foundry artifact structure, and common gotchas (tuples, uint vs uint256, as const).
When a contract changes:
The fix isn't careful manual updating — it's making the pipeline impossible to get wrong.
After every forge build, auto-export ABIs to your frontend:
bash
# scripts/sync-abi.sh
#!/bin/bash
set -e
CONTRACTS=("MyToken" "MyVault" "MyFactory")
SRC="out" # Foundry output dir
DEST="frontend/src/abis"
mkdir -p $DEST
for contract in "${CONTRACTS[@]}"; do
jq '.abi' "$SRC/$contract.sol/$contract.json" > "$DEST/$contract.json"
echo "✅ Synced $contract ABI"
done
Add to foundry.toml as a post-build hook or wire into package.json:
json
{
"scripts": {
"build:contracts": "forge build && bash scripts/sync-abi.sh",
"dev": "npm run build:contracts && next dev"
}
}
Viem's as const trick gives you full TypeScript types directly from your ABI JSON:
typescript
// abis/MyToken.ts — export from your synced JSON
export const myTokenAbi = [
{
name: 'transfer',
type: 'function',
stateMutability: 'nonpayable',
inputs: [{ name: 'to', type: 'address' }, { name: 'amount', type: 'uint256' }],
outputs: [{ name: '', type: 'bool' }],
},
// ...
] as const // ← critical: makes TypeScript infer exact types
// Now readContract/writeContract will type-check function names and args
const result = await client.readContract({
abi: myTokenAbi,
functionName: 'transfer', // ← autocompletes, typos caught at compile time
args: ['0x...', 100n], // ← arg types inferred
})
For large projects, @wagmi/cli generates fully typed React hooks from your ABIs:
bash
npm install --save-dev @wagmi/cli
typescript
// wagmi.config.ts
import { defineConfig } from '@wagmi/cli'
import { foundry, react } from '@wagmi/cli/plugins'
export default defineConfig({
out: 'src/generated.ts',
plugins: [
foundry({ project: '../contracts' }), // reads Foundry artifacts directly
react(), // generates useReadMyToken, useWriteMyToken, etc.
],
})
bash
npx wagmi generate # regenerate on every contract change
Add to CI: npx wagmi generate && git diff --exit-code src/generated.ts — fails if ABI was changed but not regenerated.
Proxy contracts (UUPS, Transparent) have two ABIs:
upgradeTo, upgradeToAndCall, admin functionsAlways use the implementation ABI for user-facing calls, pointed at the proxy address:
typescript
// WRONG: using proxy ABI loses all your functions
const client = getContract({ address: proxyAddr, abi: proxyAbi })
// RIGHT: implementation ABI + proxy address
const client = getContract({ address: proxyAddr, abi: myContractV2Abi })
For Hardhat upgrades, the generated .json artifacts include the merged ABI automatically. For Foundry, merge manually:
bash
# Merge proxy + implementation ABIs
jq -s '.[0].abi + .[1].abi | unique_by(.name)' \
out/ERC1967Proxy.sol/ERC1967Proxy.json \
out/MyContractV2.sol/MyContractV2.json \
> frontend/src/abis/MyContractProxy.json
Block merges where ABI changed but frontend wasn't updated:
yaml
# .github/workflows/abi-check.yml
- name: Check ABI sync
run: |
forge build
bash scripts/sync-abi.sh
git diff --exit-code frontend/src/abis/
# Fails if any ABI file was changed without committing the update
SymptomCauseFixfunction not found on-chainCalling old function name that was renamedRe-sync ABI, check function selectorTypeScript accepts wrong arg typeas const missing on ABIAdd as const to ABI definitionProxy call revertsUsing proxy ABI instead of implementation ABIAlways use implementation ABI at proxy addressWorks in dev, fails on mainnetABI from local build ≠ deployed contractPin ABI to verified deployment, not latest buildWagmi hook types wrongGenerated file not up to dateRe-run npx wagmi generate
references/abi-formats.md