> Feedback: If these docs are stale, missing, or confusing, post sanitized feedback to `https://docs.tempo.xyz/api/feedback` with `source: "mcp"`, a short `message`, and any relevant `toolName`, `relatedResource`, or `client`.
# Foundry for Tempo

Tempo is supported as a first-class citizen in [Foundry](https://github.com/foundry-rs/foundry): the leading Ethereum development toolkit.

Install the latest Foundry release to access Tempo's [protocol-level features](/docs/protocol) in `forge`, `cast`, `anvil`, and `chisel`, and to build, test, and deploy contracts that go [beyond the limits of standard EVM chains](/docs/quickstart/evm-compatibility).

:::warning\[`tempo-foundry` is deprecated]
`tempo-foundry` and `foundryup -n tempo` are deprecated. Switch to the latest upstream Foundry release with `foundryup`.
:::

For general information about Foundry, see the [Foundry documentation](https://getfoundry.sh/).

## Get started with Foundry

Install the latest Foundry release to get Tempo support.

:::::steps

### Install `foundryup`

If you don't have `foundryup` installed yet:

```bash
curl -L https://foundry.paradigm.xyz | bash
```

### Install or update Foundry

```bash
foundryup
```

This installs the latest versioned release of [`forge`](https://getfoundry.sh/forge/overview#forge), [`cast`](https://getfoundry.sh/cast/overview#cast), [`anvil`](https://getfoundry.sh/anvil/overview#anvil), and [`chisel`](https://getfoundry.sh/chisel/overview#chisel).

:::tip
To install a specific version, replace `<TAG_NAME>` with the desired release tag:

```bash
foundryup --install <TAG_NAME>
```

:::

### Create a new Foundry project

Initialize a new Foundry project with the Tempo template:

```bash
forge init -n tempo my-project && cd my-project
```

This 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`](https://github.com/tempoxyz/tempo-std) manually:

```bash
forge install tempoxyz/tempo-std
```

:::::

## Configure `foundry.toml`

The Tempo template gives you a working starting point, but it is often useful to make Tempo explicit in `foundry.toml`.

### Configure RPC aliases

Set a default Tempo RPC alias and keep a separate alias for Moderato testnet:

```toml
[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:

```toml
[profile.default]
tempo = true
```

This 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.

### Verification config

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.

### Using `foundry-toolchain` in your CI

Use the [`foundry-toolchain`](https://github.com/foundry-rs/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.

```yaml
- name: Install Foundry
  uses: foundry-rs/foundry-toolchain@v1
```

## Use Foundry for your workflows

All standard Foundry commands are supported out of the box.

### Test & deploy with `forge` locally

```bash
# Build your contracts
forge build

# Run all tests locally
forge test

# Run deployment scripts locally
forge script script/Mail.s.sol
```

### Test & deploy with `forge` on Tempo Testnet

```bash
# 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_KEY
```

Use 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](/docs/quickstart/verify-contracts).

:::warning\[Batch Transaction Rules]

* **Atomic execution**: If any call reverts, the entire batch reverts
* **Single CREATE allowed**: At most one contract deployment per batch
* **CREATE must be first**: Deployment must be the first operation
* **Value must be zero**: Since Tempo has no native token, value must be 0
* **Silent failures**: Calling a non-existent function without a fallback succeeds silently
  :::

### Interact & debug with `cast`

```bash
# 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_ADDRESS
```

If 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 Development with Anvil

Anvil supports Tempo mode for local testing and forking Tempo networks:

```bash
# 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_KEY
```

## Tempo-Specific 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

`cast keychain` provides a CLI interface to Tempo's [Account Keychain precompile](/docs/protocol/transactions/AccountKeychain).

Prefer this over hand-encoding `authorizeKey(...)` calldata when you are working from the CLI.

:::info
`cast keychain` only works on Tempo networks.
:::

`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`.

```bash
# 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_URL
```
