---
id: TIP-1034
title: TIP-20 Channel Escrow Precompile
description: Enshrines TIP-20 channel escrow as a Tempo precompile with payment-lane admission and native escrow transfer semantics.
authors: Tanishk Goyal, Brendan Ryan
status: Draft
related: TIP-20, TIP-1000, TIP-1009, TIP-1020, Tempo Session, Tempo Charge
protocolVersion: TBD
---

# TIP-1034: TIP-20 Channel Escrow Precompile

## Abstract

This TIP enshrines TIP-20 channel escrow in Tempo as a native precompile. The implementation follows the existing reference channel model (`open`, `settle`, `topUp`, `requestClose`, `close`, `withdraw`) and keeps the same EIP-712 voucher flow, with explicit partial-capture updates defined in this spec.

The precompile is introduced to reduce execution overhead, remove the separate `approve` UX via native escrow movement, and make channel operations first-class payment-lane traffic under congestion.

## Motivation

TIP-20 channel escrow is currently specified as a Solidity contract reference implementation. Enshrining that behavior as a precompile is motivated by three protocol-level goals:

1. **Efficiency**: Channel operations become native precompile execution instead of generic contract execution, reducing overhead and making gas behavior more predictable for high-frequency payment flows.
2. **Canonical implementation, evolved at network-upgrade cadence**: The escrow lives at a single canonical address on every Tempo node (`0x4D50500…`, ASCII "MPP"), with no admin or proxy admin, and ships behavior via network upgrades alongside the rest of the protocol. Wallets, indexers, and partner integrations bind to one stable surface rather than to a moving set of redeployments.
3. **Approve-less Escrow UX**: `open` and `topUp` can escrow TIP-20 funds through native system transfer (`systemTransferFrom` semantics), removing the extra `approve` transaction from the user flow and works uniformly for passkey accounts.

This produces a simpler and more reliable path for session and auth/capture style integrations without changing the core channel model developers already use.

---

# Specification

## Precompile Address

This TIP introduces a new system precompile at:

```solidity
address constant TIP20_CHANNEL_ESCROW = 0x4D50500000000000000000000000000000000000;
```

`TIP20_CHANNEL_ESCROW` MUST refer to this address throughout this specification.

## Implementation Details

This TIP is normative. The current reference Solidity artifacts are informative and live at:

1. [`tips/verify/src/interfaces/ITIP20ChannelEscrow.sol`](verify/src/interfaces/ITIP20ChannelEscrow.sol)
2. [`tips/verify/src/TIP20ChannelEscrow.sol`](verify/src/TIP20ChannelEscrow.sol)

Implementations SHOULD keep those reference artifacts aligned with the normative interface and execution rules defined below.

Channels are unidirectional (`payer -> payee`) and token-specific.

### Channel Identity And Packed State

```solidity
struct ChannelDescriptor {
    address payer;
    address payee;
    address operator;
    address token;
    bytes32 salt;
    address authorizedSigner;
    bytes32 expiringNonceHash;
}

struct ChannelState {
    uint96 settled;
    uint96 deposit;
    uint32 closeRequestedAt;
}

struct Channel {
    ChannelDescriptor descriptor;
    ChannelState state;
}
```

Implementations MUST store only `ChannelState` on-chain, keyed by `channelId` and packed into a
single 32-byte storage slot. `ChannelDescriptor` is immutable channel identity and MUST NOT be
stored on-chain; it MUST instead be supplied in calldata for post-open operations and emitted in
`ChannelOpened` so indexers and counterparties can recover it.

The packed slot layout is:

```text
bits   0..95   settled        uint96
bits  96..191  deposit        uint96
bits 192..223  closeRequestedAt uint32
bits 224..255  reserved       zero
```

These widths are chosen to keep the entire mutable channel state in one storage slot without
introducing any practical limit for production usage. `uint96` supports up to
`2^96 - 1 = 79,228,162,514,264,337,593,543,950,335` base units, which is far above the supply or
escrow size of any realistic TIP-20 token deployment. `uint32` stores second-resolution unix
timestamps through February 2106, which is sufficient for close-request tracking because channels
are expected to live for minutes, hours, days, or months rather than many decades. The reserved
high 32 bits MUST remain zero.

`closeRequestedAt` MUST be encoded as:

1. `0` for an active channel with no close request.
2. Any non-zero value for an active channel with a close request, where the stored value is the
   exact close-request timestamp.

Closed channels MUST not retain any packed `ChannelState` slot at all.

`channelId` MUST use the following deterministic domain-separated construction:

```solidity
channelId = keccak256(
    abi.encode(
        payer,
        payee,
        operator,
        token,
        salt,
        authorizedSigner,
        expiringNonceHash,
        TIP20_CHANNEL_ESCROW,
        block.chainid
    )
);
```

For `open`, `expiringNonceHash` MUST be the replay-protected context hash of the enclosing
channel-open transaction:

```solidity
expiringNonceHash = keccak256(abi.encodePacked(encodeForSigning, sender));
```

Here `encodeForSigning` is the exact byte payload whose hash is signed by the top-level transaction
sender, and `sender` is the top-level transaction sender address. For Tempo AA transactions, this
is exactly the existing expiring nonce hash construction: the signing payload omits the actual
fee-payer signature, and when a fee payer is present it also omits the fee token chosen by the fee
payer. For legacy, EIP-2930, EIP-1559, and EIP-7702 transactions, `encodeForSigning` is the usual
unsigned transaction signing payload.

`open` MUST reject if the enclosing execution context does not have this context hash available.
For real signed transactions, the protocol always has both the sender and the corresponding signing
payload.

For all post-open operations, `expiringNonceHash` MUST be supplied via `ChannelDescriptor` so the
implementation can recompute the same `channelId` without storing immutable descriptor fields
on-chain.

### Same-Transaction Opens

Multiple `open` calls MAY appear in the same top-level transaction, including a Tempo AA batch, as
long as each call derives a distinct `channelId`. Because all calls in the same top-level
transaction share the same `expiringNonceHash` context value, distinct same-transaction opens MUST
differ in at least one other descriptor field, such as `payee`, `operator`, `token`, `salt`, or
`authorizedSigner`.

This is safe because vouchers are bound to the resulting `channelId`, and each distinct
`channelId` has independent escrow state. For example, an AA transaction MAY atomically open
several channels for different sessions or payees.

An `open` call MUST NOT succeed if the derived `channelId` was already opened earlier in the same
top-level transaction. This requirement covers both ordinary duplicate opens and the otherwise
dangerous `open -> close` or `open -> withdraw` followed by a second `open` with the same
descriptor. Terminal `close` and `withdraw` delete persistent channel state, so implementations MUST
also maintain transient per-transaction opened-channel tracking to reject such same-transaction
reopens after deletion.

Reopening the same logical descriptor in a later transaction is allowed after terminal closure,
because the later transaction has a different replay-protected context hash and therefore derives a
different `channelId`.

### Interface

The canonical interface for this TIP is [`tips/verify/src/interfaces/ITIP20ChannelEscrow.sol`](verify/src/interfaces/ITIP20ChannelEscrow.sol).

Implementations MUST expose an external interface that is semantically identical to that file,
including the `ChannelDescriptor`, `ChannelState`, and `Channel` structs, the descriptor-based
post-open methods, the events, the errors, and the function signatures.

### Execution Semantics

Voucher signatures use the following EIP-712 type:

```solidity
Voucher(bytes32 channelId,uint96 cumulativeAmount)
```

Voucher signatures MUST be verified via the TIP-1020 Signature Verification Precompile at
`0x5165300000000000000000000000000000000000`, using the same signature encodings and
verification rules as Tempo transaction signatures.

This means:

1. Implementations MUST compute the EIP-712 voucher digest and validate signatures using TIP-1020 `recover` or `verify`, not raw `ecrecover`.
2. Voucher signatures MAY use any TIP-1020-supported Tempo signature type.
3. TIP-1020 keychain wrapper signatures (`0x03` / `0x04`) MUST be rejected for direct voucher verification.
4. Delegated voucher signing MUST use `authorizedSigner`, rather than a keychain wrapper around `payer`.

Execution semantics use exact timestamp boundaries. Close-grace completion MUST use the strict
predicate `block.timestamp >= closeRequestedAt + CLOSE_GRACE_PERIOD`. Implementations MUST NOT
substitute a different comparison predicate.

`operator` is an immutable payee-side authority for the channel. `address(0)` means the payee is
the only payee-side authority. A nonzero `operator` MAY submit `settle` and `close` on the payee's
behalf, but all settlement and close-capture payouts still transfer to `payee`.

Execution semantics are:

1. `open` MUST reject zero deposit, invalid token address, and invalid payee address.
2. `open` MUST derive `expiringNonceHash` from the enclosing transaction's replay-protected context hash, persist only the packed `ChannelState` slot, and MUST emit the full immutable descriptor in `ChannelOpened`.
3. Post-open methods (`settle`, `topUp`, `close`, `requestClose`, `withdraw`, and descriptor-based views) MUST recompute `channelId` from the supplied descriptor and use that derived id for storage lookup.
4. If `closeRequestedAt != 0`, a successful `topUp` MUST clear it back to `0` and emit `CloseRequestCancelled`.
5. `requestClose` MUST set `closeRequestedAt = uint32(block.timestamp)` on the first successful call and leave it unchanged on later successful calls.
6. `settle` and `close` MUST be callable only by `payee`, or by `operator` when `operator != address(0)`.
7. `close` MUST validate the voucher signature via TIP-1020 for any capture-increasing close.
8. Signer MUST be `authorizedSigner` from the supplied descriptor when set, otherwise `payer` from the supplied descriptor.
9. `close` MUST enforce `previousSettled <= captureAmount <= cumulativeAmount`.
10. `close` MUST reject when `captureAmount > deposit`, even if `cumulativeAmount > deposit`.
11. A `close` voucher with `cumulativeAmount > deposit` remains valid for signature verification; `captureAmount` is the escrow-bounded amount that may actually be paid out.
12. `close` MUST settle `captureAmount - previousSettled` to payee and refund `deposit - captureAmount` to payer.
13. `withdraw` MUST be allowed only when the close grace period has elapsed from `closeRequestedAt`.
14. Terminal `close` and `withdraw` MUST delete the stored slot entirely. Reopening the same logical channel in a later transaction MUST produce a different `channelId` because the replay-protected context hash changes.
15. Within one top-level transaction, `open` MUST reject any `channelId` that was already opened earlier in that same transaction, even if the channel was terminally closed or withdrawn before the later `open` call.

The `cumulativeAmount > deposit` allowance is specific to `close`, because `close` has a separate
`captureAmount` parameter. The voucher proves that the payer authorized at least the captured
amount, while `captureAmount` chooses the final escrow-bounded payout. `settle` does not have a
separate capture parameter, so its `cumulativeAmount` is the exact new settled amount and MUST
remain bounded by the current deposit.

## Native Escrow Movement

In this precompile, escrow transfers MUST use system TIP-20 movement semantics equivalent to `systemTransferFrom`.

Required behavior:

1. `open` escrows `deposit` from `payer` to channel escrow state without requiring a prior user `approve` transaction.
2. `topUp` escrows `additionalDeposit` the same way.
3. `settle`, `close`, and `withdraw` payout paths continue to transfer TIP-20 value using protocol-native token movement.

### No Separate Emergency Close

This TIP does not define a separate `emergencyClose` entrypoint for ordinary recipient-specific
`close` failures.

`close` is the only channel operation that atomically performs both outbound payout legs in one
call: `escrow -> payee` and `escrow -> payer`. If that combined path fails only because one
recipient leg cannot receive funds under the token's current transfer policy, the unaffected party
already has a unilateral single-recipient fallback:

1. If the payer-side refund leg makes `close` unusable, the payee can continue using `settle`
   subject to the normal `settle` bounds.
2. If the payee-side payout leg makes `close` unusable, the payer can recover the remaining escrow
   via `requestClose` + `withdraw`.

This means a recipient-specific `close` failure does not create a new bilateral hostage condition,
so a dedicated `emergencyClose` is not required for that case.

This reasoning is limited to recipient-specific `close` failures. It does not change the general
requirement that `settle`, `close`, and `withdraw` all rely on protocol-native TIP-20 payout
transfers. If the token's pause state or transfer policy later prevents the escrow address from
sending funds at all, all outbound exit paths can fail.

## Payment-Lane Integration (Mandatory)

Channel escrow operations MUST be treated as payment-lane transactions in consensus classification, pool admission, and payload building.

### Classification Rules

Implementations MUST define a strict classifier `is_channel_escrow_payment(to, input)` that returns true iff:

1. `to == TIP20_CHANNEL_ESCROW`.
2. `input` selector is one of `{open, settle, topUp, close, requestClose, withdraw}`.
3. Calldata length/encoding is valid for that selector.
4. For `settle` and `close`, the trailing `signature` bytes use a valid TIP-1020 / Tempo transaction signature encoding.

Transactions with authorization side effects MUST be classified as non-payment.

For EIP-7702 transactions, payment classification requires `authorization_list.length == 0`.

For AA transactions, payment classification MUST satisfy all of:

1. `calls.length > 0`.
2. `tempo_authorization_list.length == 0`.
3. `key_authorization` is absent.
4. Every call satisfies either TIP-20 strict payment classification or `is_channel_escrow_payment`.

An AA transaction with an empty `calls` array MUST be classified as non-payment.

### Required Integration Points

1. The consensus-level payment classifier (`is_payment`) MUST include TIP-20 channel escrow calls and MUST enforce the same authorization-side-effect exclusions above.
2. The strict builder/pool classifier (`is_payment_v2`) MUST include `is_channel_escrow_payment` and MUST enforce the same authorization-side-effect exclusions above.
3. The transaction pool payment flag MUST be computed from the strict classifier, so channel escrow calls are admitted in the payment lane path.
4. The payload builder non-payment gate (`general_gas_limit` enforcement) MUST treat channel escrow calls as payment, so they are not rejected by the non-payment overflow path.

### What This Means Operationally

With this integration, channel lifecycle calls consume payment-lane capacity rather than non-payment capacity. Under high congestion, these transactions continue to compete in the same lane as other payment traffic instead of being excluded by non-payment limits.

---

# Invariants

1. `settled <= deposit` MUST hold in all reachable states.
2. `settled` is monotonically non-decreasing.
3. Any successful capture (`settle` or `close`) MUST be authorized by a valid voucher signature from the expected signer.
4. Only payer can `topUp`, `requestClose`, and `withdraw`.
5. Only payee, or a nonzero operator, can `settle` and `close`.
6. A zero operator does not authorize any caller.
7. A channel MUST consume exactly one storage slot of mutable on-chain state.
8. `closeRequestedAt == 0` MUST mean active with no close request.
9. `closeRequestedAt != 0` MUST mean active with a close request timestamp equal to `closeRequestedAt`.
10. Closed channels MUST have no remaining mutable on-chain state.
11. Reopening the same logical channel in a later transaction MUST yield a different `channelId` because the replay-protected context hash is different.
12. Reopening the same `channelId` within one top-level transaction MUST be impossible.
13. Fund conservation MUST hold at all terminal states.
14. Channel escrow calls MUST be classified as payment transactions in both consensus and strict builder/pool classifiers, AA payment classification MUST require `calls.length > 0`, and transactions with authorization side effects MUST be classified as non-payment.
15. `open` and `topUp` MUST not require a prior user `approve` transaction.
16. `withdraw` MUST require an active close request whose grace period has elapsed.
17. `close` MUST enforce `previousSettled <= captureAmount <= cumulativeAmount`, and `captureAmount <= deposit`.

## References

- [TIP-20](https://docs.tempo.xyz/protocol/tip20/spec)
- [TIP-1000](tip-1000.md)
- [TIP-1009](tip-1009.md)
- [TIP-1020](tip-1020.md)
- [Tempo Session Intent for HTTP Payment Authentication](https://paymentauth.org/draft-tempo-session-00.html)
- [Tempo Charge Intent for HTTP Payment Authentication](https://paymentauth.org/draft-tempo-charge-00.html)
