Foundry for Tempo
Tempo is supported as a first-class citizen in Foundry: the leading Ethereum development toolkit.
Install the latest Foundry release to access Tempo's protocol-level features in forge, cast, anvil, and chisel, and to build, test, and deploy contracts that go beyond the limits of standard EVM chains.
For general information about Foundry, see the Foundry documentation.
Get started with Foundry
Install the latest Foundry release to get Tempo support.
Install foundryup for Tempo
If you don't have foundryup installed yet:
curl -L https://foundry.paradigm.xyz | bashCreate a new Foundry project
Initialize a new Foundry project with the Tempo template:
forge init -n tempo my-project && cd my-projectThis gives you a Tempo-ready starter project, including the Tempo Mail example template.
If you're adding Tempo support to an existing Foundry project, install tempo-std manually:
forge install tempoxyz/tempo-stdConfigure foundry.toml for Tempo
The Tempo template gives you a working starting point, but it is often useful to make Tempo explicit in foundry.toml.
Configure Tempo RPC aliases
Set a default Tempo RPC alias and keep a separate alias for Moderato testnet:
[profile.default]
eth_rpc_url = "tempo"
[rpc_endpoints]
tempo = "${TEMPO_RPC_URL}"
moderato = "${TEMPO_TESTNET_RPC_URL}"With this config, commands that use the default RPC pick up tempo automatically, and you can still switch explicitly with --rpc-url moderato.
Activate Tempo features explicitly
For most projects, the most flexible option is to enable Tempo network features directly:
[profile.default]
tempo = trueThis network flag enables Tempo-specific network behavior while still letting Foundry resolve the right semantics from the chain you are targeting.
If you need advanced testing against historical network behavior, pin a specific Tempo hardfork explicitly in foundry.toml or via inline config. Most projects should avoid hardfork pinning so local tests track the current Tempo rules for the network they target.
Configure Tempo contract verification
Tempo's contract verifier is Sourcify-compatible. Configure verification with VERIFIER_URL=https://contracts.tempo.xyz or --verifier-url https://contracts.tempo.xyz.
The [etherscan] table in foundry.toml is for Etherscan-style verifiers, not Tempo's verifier.
Use foundry-toolchain in Tempo CI
Use the foundry-toolchain GitHub Action to install Foundry in your CI.
Tempo support is included in the latest Foundry release, so no special configuration is needed.
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1Use Foundry for Tempo workflows
All standard Foundry commands are supported out of the box.
Test and deploy Tempo contracts locally with forge
# Build your contracts
forge build
# Run all tests locally
forge test
# Run deployment scripts locally
forge script script/Mail.s.solTest and deploy with forge on Tempo Testnet
# Set environment variables
export TEMPO_RPC_URL=https://rpc.moderato.tempo.xyz
export VERIFIER_URL=https://contracts.tempo.xyz
# Optional: create a new keypair and request some testnet tokens from the faucet.
cast wallet new
cast rpc tempo_fundAddress <YOUR_WALLET_ADDRESS> --rpc-url https://rpc.moderato.tempo.xyz
# Run all tests on Tempo's testnet
forge test
# Deploy and verify a simple contract
forge create src/Mail.sol:Mail \
--rpc-url $TEMPO_RPC_URL \
--interactive \
--broadcast \
--verify \
--constructor-args 0x20c0000000000000000000000000000000000001
# Deploy a simple contract with custom fee token
forge create src/Mail.sol:Mail \
--tempo.fee-token <FEE_TOKEN_ADDRESS> \
--rpc-url $TEMPO_RPC_URL \
--interactive \
--broadcast \
--verify \
--constructor-args 0x20c0000000000000000000000000000000000001
# Set a salt for deterministic contract address derivation
# The salt is passed to TIP20_FACTORY.createToken() which uses it with the sender
# address to compute a deterministic deployment address via getTokenAddress(sender, salt)
export SALT="my-unique-salt"
# Run a deployment script and verify
forge script script/Mail.s.sol \
--sig "run(string)" $SALT \
--rpc-url $TEMPO_RPC_URL \
--interactive \
--sender <YOUR_WALLET_ADDRESS> \
--broadcast \
--verify
# Run a deployment script with custom fee token and verify
forge script script/Mail.s.sol \
--sig "run(string)" $SALT \
--tempo.fee-token <FEE_TOKEN_ADDRESS> \
--rpc-url $TEMPO_RPC_URL \
--interactive \
--sender <YOUR_WALLET_ADDRESS> \
--broadcast \
--verify
# Batch multiple calls into a single atomic transaction
forge script script/Deploy.s.sol \
--broadcast --batch \
--rpc-url $TEMPO_RPC_URL \
--private-key $PRIVATE_KEYUse a root key for forge create. Access keys can sign calls but not deployments.
For more verification options including verifying existing contracts and API verification, see Contract Verification.
Interact and debug Tempo contracts with cast
# Check that your contract is deployed:
cast code <CONTRACT_ADDRESS> \
--rpc-url $TEMPO_RPC_URL
# Interact with the contract, retrieving the token address:
cast call <CONTRACT_ADDRESS> "token()" \
--rpc-url $TEMPO_RPC_URL
# Get the name of an ERC20 token:
cast erc20 name <TOKEN_ADDRESS> \
--rpc-url $TEMPO_RPC_URL
# Check the ERC20 token balance of your address:
cast erc20 balance <TOKEN_ADDRESS> <YOUR_WALLET_ADDRESS> \
--rpc-url $TEMPO_RPC_URL
# Transfer some of your ERC20 tokens:
cast erc20 transfer <TOKEN_ADDRESS> <RECEIVER_ADDRESS> <AMOUNT> \
--rpc-url $TEMPO_RPC_URL \
--interactive
# Transfer some of your ERC20 tokens with custom fee token:
cast erc20 transfer <TOKEN_ADDRESS> <RECEIVER_ADDRESS> <AMOUNT> \
--tempo.fee-token <FEE_TOKEN_ADDRESS> \
--rpc-url $TEMPO_RPC_URL \
--interactive
# Send a transaction with custom fee token:
cast send <CONTRACT_ADDRESS> <FUNCTION_SIGNATURE> \
--tempo.fee-token <FEE_TOKEN_ADDRESS> \
--rpc-url $TEMPO_RPC_URL \
--interactive
# Replay a transaction by hash:
cast run <TX_HASH> \
--rpc-url $TEMPO_RPC_URL
# Send a batch transaction with multiple calls:
cast batch-send \
--call "<CONTRACT_ADDRESS>::increment()" \
--call "<CONTRACT_ADDRESS>::setNumber(uint256):500" \
--rpc-url $TEMPO_RPC_URL \
--private-key $PRIVATE_KEY
# Batch with pre-encoded calldata:
ENCODED=$(cast calldata "setNumber(uint256)" 200)
cast batch-send \
--call "<CONTRACT_ADDRESS>::$ENCODED" \
--call "<CONTRACT_ADDRESS>::setNumber(uint256):101" \
--rpc-url $TEMPO_RPC_URL \
--private-key $PRIVATE_KEY
# Sponsored transaction (gasless for sender):
# Step 1: Get the fee payer signature hash
FEE_PAYER_HASH=$(cast mktx <CONTRACT_ADDRESS> 'increment()' --rpc-url $TEMPO_RPC_URL --private-key $SENDER_KEY --tempo.print-sponsor-hash)
# Step 2: Sponsor signs the hash
SPONSOR_SIG=$(cast wallet sign --private-key $SPONSOR_KEY "$FEE_PAYER_HASH" --no-hash)
# Step 3: Send with sponsor signature
cast send <CONTRACT_ADDRESS> 'increment()' --rpc-url $TEMPO_RPC_URL --private-key $SENDER_KEY --tempo.sponsor-signature "$SPONSOR_SIG"
# Send with 2D nonce (parallel tx submission):
cast send <CONTRACT_ADDRESS> 'increment()' \
--rpc-url $TEMPO_RPC_URL \
--private-key $PRIVATE_KEY \
--nonce 0 --tempo.nonce-key 1
# Send with expiring nonce (time-bounded tx, max 30s):
VALID_BEFORE=$(($(date +%s) + 25))
cast send <CONTRACT_ADDRESS> 'increment()' \
--rpc-url $TEMPO_RPC_URL \
--private-key $PRIVATE_KEY \
--tempo.expiring-nonce --tempo.valid-before $VALID_BEFORE
# Send with access key (delegated signing):
# First authorize the key via Account Keychain precompile
cast send 0xAAAAAAAA00000000000000000000000000000000 \
'authorizeKey(address,uint8,(uint64,bool,(address,uint256,uint64)[],bool,(address,(bytes4,address[])[])[]))' \
$ACCESS_KEY_ADDR 0 '(1893456000,false,[],true,[])' \
--rpc-url $TEMPO_RPC_URL \
--private-key $ROOT_PRIVATE_KEY
# Then send using the access key
cast send <CONTRACT_ADDRESS> 'increment()' \
--rpc-url $TEMPO_RPC_URL \
--tempo.access-key $ACCESS_KEY_PRIVATE_KEY \
--tempo.root-account $ROOT_ADDRESSIf the access key will be used with passkey or WebAuthn signatures, pass 2 for SignatureType. 1 is only for raw P256 signatures.
Access-key transactions cannot create contracts, so use a root key for deployments or other flows that perform CREATE.
Local Tempo development with Anvil
Anvil supports Tempo mode for local testing and forking Tempo networks:
# Start anvil in Tempo mode
anvil --tempo
# Fork a live Tempo network for local testing
anvil --tempo --fork-url $TEMPO_RPC_URL
# Test transactions on local anvil fork
cast send <CONTRACT_ADDRESS> 'increment()' \
--tempo.fee-token <FEE_TOKEN_ADDRESS> \
--rpc-url http://127.0.0.1:8545 \
--private-key $PRIVATE_KEY
# 2D nonce on anvil fork
cast send <CONTRACT_ADDRESS> 'increment()' \
--tempo.fee-token <FEE_TOKEN_ADDRESS> \
--rpc-url http://127.0.0.1:8545 \
--private-key $PRIVATE_KEY \
--nonce 0 --tempo.nonce-key 100
# Expiring nonce on anvil fork
cast send <CONTRACT_ADDRESS> 'increment()' \
--tempo.fee-token <FEE_TOKEN_ADDRESS> \
--rpc-url http://127.0.0.1:8545 \
--private-key $PRIVATE_KEY \
--tempo.expiring-nonce --tempo.valid-before $(($(date +%s) + 25))
# Batch transactions on anvil fork
cast batch-send \
--tempo.fee-token <FEE_TOKEN_ADDRESS> \
--rpc-url http://127.0.0.1:8545 \
--call "<CONTRACT_ADDRESS>::increment()" \
--call "<CONTRACT_ADDRESS>::increment()" \
--private-key $PRIVATE_KEYTempo-specific Foundry CLI flags
The following flags are available for cast and forge script for Tempo-specific features:
| Flag | Description | Example |
|---|---|---|
--tempo.fee-token <ADDRESS> | Specify the TIP-20 token to pay transaction fees | --tempo.fee-token 0x20c0...0001 |
--tempo.nonce-key <KEY> | 2D nonce key for parallel transaction submission | --tempo.nonce-key 1 |
--tempo.expiring-nonce | Enable expiring nonce for time-bounded transactions | --tempo.expiring-nonce |
--tempo.valid-before <TIMESTAMP> | Unix timestamp before which tx must execute (max 30s from now) | --tempo.valid-before 1704067200 |
--tempo.valid-after <TIMESTAMP> | Unix timestamp after which tx can execute | --tempo.valid-after 1704067100 |
--tempo.sponsor-signature <SIG> | Pre-signed sponsor signature for gasless transactions | --tempo.sponsor-signature 0x... |
--tempo.print-sponsor-hash | Print fee payer signature hash and exit (for sponsor to sign) | --tempo.print-sponsor-hash |
--tempo.access-key <KEY> | Private key for delegated signing via access key | --tempo.access-key $ACCESS_KEY_PRIVATE_KEY |
--tempo.root-account <ADDRESS> | Root account address when using an access key | --tempo.root-account $ROOT_ADDRESS |
Ledger and Trezor wallets are not yet compatible with any --tempo.* option.
cast keychain for Tempo access keys
cast keychain provides a CLI interface to Tempo's Account Keychain precompile.
Prefer this over hand-encoding authorizeKey(...) calldata when you are working from the CLI.
cast keychain authorization takes a future expiry timestamp, webauthn for passkey-backed access keys, optional TOKEN:AMOUNT:PERIOD_SECONDS limits for recurring budgets, and --scope for target, selector, and recipient restrictions.
cast keychain sends the Account Keychain ABI directly, so a non-expiring key uses 18446744073709551615 (type(uint64).max) as the expiry value. This differs from tx-level key_authorization, where a non-expiring key is represented by omitting expiry. Do not pass 0.
# Access keys must be authorized with a future expiry timestamp.
EXPIRY=$(($(date +%s) + 86400))
# For a non-expiring key via direct precompile ABI / cast keychain, use:
NEVER_EXPIRES=18446744073709551615
# Authorize a new access key (signature types: secp256k1, p256, webauthn):
cast keychain authorize <KEY_ID> secp256k1 $EXPIRY \
--rpc-url $TEMPO_RPC_URL \
--private-key $PRIVATE_KEY
# Authorize with a spending limit (TOKEN:AMOUNT or TOKEN:AMOUNT:PERIOD_SECONDS):
cast keychain authorize <KEY_ID> secp256k1 $EXPIRY \
--limit <TOKEN_ADDRESS>:1000000 \
--rpc-url $TEMPO_RPC_URL \
--private-key $PRIVATE_KEY
# Authorize with call scopes (restrict to specific contracts/functions):
cast keychain authorize <KEY_ID> secp256k1 $EXPIRY \
--scope <TOKEN_ADDRESS>:transfer,approve \
--rpc-url $TEMPO_RPC_URL \
--private-key $PRIVATE_KEY
# Authorize with call scope restricted to a specific recipient:
cast keychain authorize <KEY_ID> secp256k1 $EXPIRY \
--scope <TOKEN_ADDRESS>:transfer@<RECIPIENT_ADDRESS> \
--rpc-url $TEMPO_RPC_URL \
--private-key $PRIVATE_KEY
# Full example: 24h expiry + spending limit + call scope:
cast keychain authorize <KEY_ID> secp256k1 $EXPIRY \
--limit <TOKEN_ADDRESS>:1000000 \
--scope <TOKEN_ADDRESS>:transfer \
--rpc-url $TEMPO_RPC_URL \
--private-key $PRIVATE_KEY
# Revoke an access key (permanent, cannot be re-authorized):
cast keychain revoke <KEY_ID> \
--rpc-url $TEMPO_RPC_URL \
--private-key $PRIVATE_KEY
# Update spending limit for a key-token pair:
cast keychain update-limit <KEY_ID> <TOKEN_ADDRESS> <NEW_LIMIT> \
--rpc-url $TEMPO_RPC_URL \
--private-key $PRIVATE_KEY
# Replace all call scopes for a key:
cast keychain set-scope <KEY_ID> \
--scope <TOKEN_ADDRESS>:transfer \
--scope <CONTRACT_ADDRESS> \
--rpc-url $TEMPO_RPC_URL \
--private-key $PRIVATE_KEY
# Remove a target contract from allowed call list:
cast keychain remove-scope <KEY_ID> <TARGET_ADDRESS> \
--rpc-url $TEMPO_RPC_URL \
--private-key $PRIVATE_KEY
# Query key provisioning status (read-only):
cast keychain check <ACCOUNT> <KEY_ID> \
--rpc-url $TEMPO_RPC_URL
# Query remaining spending limit via the precompile directly:
cast call 0xAAAAAAAA00000000000000000000000000000000 \
'getRemainingLimitWithPeriod(address,address,address)(uint256,uint64)' \
<ACCOUNT> <KEY_ID> <TOKEN_ADDRESS> \
--rpc-url $TEMPO_RPC_URLWas this helpful?