Skip to main content
Pre-Release Documentation

This describes a pre-release version of LBAMM. Interfaces and behavior may change.

Verify the exact repository commit before building production integrations.

Permit Transfer Handler

The PermitTransferHandler is a transfer handler that settles swap input tokens using PermitC-style permits. It allows a swap executor to supply input tokens to the AMM without relying on prior ERC-20 approvals, while binding the authorization to specific swap intent.

This handler supports two permit modes:

  • Fill-or-kill permits: must be executed atomically for the exact filled amount
  • Partial-fill permits: may be filled incrementally, with onchain fill-state tracked by PermitC

This page describes:

  • When to use the PermitTransferHandler
  • How transferData / transferExtraData are encoded
  • What the permit signature is bound to
  • Cosigner and onchain executor validation
  • How to construct swaps and signatures (Solidity + viem)

What PermitTransferHandler does

During swap finalization, the AMM calls the PermitTransferHandler to supply the required input tokens using a PermitC-style permit.

At a high level, the handler:

  • transfers the final net amountIn of swapOrder.tokenIn into the AMM
  • validates a PermitC signature bound to swap intent via an additional data hash
  • enforces atomic fill (fill-or-kill) or bounded incremental fill (partial fill)

The handler is compatible with any permit processor that implements the PermitC interface expected by the handler.


Encoding: transferData and transferExtraData

All LBAMM swap entrypoints accept bytes transferData. PermitTransferHandler uses it as follows:

  • The first 32 bytes are abi.encode(address(transferHandler))
  • The remaining bytes are passed as transferExtraData to the handler

For PermitTransferHandler, transferExtraData is:

  • 1 byte permit type discriminator
  • followed by abi.encode(...) of the corresponding permit struct

Permit type discriminators

bytes1 constant FILL_OR_KILL_PERMIT = 0x00;
bytes1 constant PARTIAL_FILL_PERMIT = 0x01;

Permit structs

struct FillOrKillPermitTransfer {
address permitProcessor;
address from;
uint256 nonce;
uint256 permitAmount;
uint256 expiration;
bytes signature;
address cosigner;
uint256 cosignatureExpiration;
bytes cosignature;
address hook;
bytes hookData;
}

struct PartialFillPermitTransfer {
address permitProcessor;
address from;
uint256 salt;
int256 permitAmountSpecified;
uint256 permitLimitAmount;
uint256 expiration;
bytes signature;
address cosigner;
uint256 cosignatureExpiration;
uint256 cosignatureNonce;
bytes cosignature;
address hook;
bytes hookData;
}

Fill-or-kill vs partial-fill semantics

Fill-or-kill permits

A fill-or-kill permit must match the swap’s filled amount exactly:

  • If the swap is output-based (swapOrder.amountSpecified < 0), the filled amountOut must equal -swapOrder.amountSpecified.
  • If the swap is input-based (swapOrder.amountSpecified > 0), the filled amountIn must equal swapOrder.amountSpecified.

If the swap does not fill exactly as specified, the handler reverts.

Partial-fill permits

Partial-fill permits support incremental fills with PermitC tracking fill state. The handler additionally enforces that:

  • The permit’s swap mode (input vs output based) matches the swap order’s mode
  • The partial fill does not exceed the maximum allowed input for the realized output

This prevents executors from over-consuming tokenIn relative to the signed intent.


What the signature is bound to

PermitTransferHandler uses PermitC permits that validate an additional data hash derived from swap intent.

Conceptually, the hash is built over the following EIP-712 Swap payload:

struct Swap {
bool partialFill;
address recipient;
int256 amountSpecified;
uint256 limitAmount;
address tokenOut;
address exchangeFeeRecipient;
uint16 exchangeFeeBPS;
address cosigner;
address hook;
}

The hash is included in the PermitC approval type via the following typehash construction:

PERMITTED_TRANSFER_APPROVAL_TYPEHASH = keccak256(
bytes.concat(
bytes(PERMITTED_TRANSFER_APPROVAL_TYPEHASH_STUB),
bytes(PERMITTED_APPROVAL_TYPEHASH_EXTRADATA_STUB),
bytes(SWAP_TYPEHASH_STUB)
)
);

PERMITTED_ORDER_APPROVAL_TYPEHASH = keccak256(
bytes.concat(
bytes(PERMITTED_ORDER_APPROVAL_TYPEHASH_STUB),
bytes(PERMITTED_APPROVAL_TYPEHASH_EXTRADATA_STUB),
bytes(SWAP_TYPEHASH_STUB)
)
);

Executor authorization: cosigner vs onchain hook

PermitTransferHandler supports two optional layers of executor authorization.

Cosigner (offchain authorization)

A permit may include a cosigner and cosignature.

  • If cosigner == address(0), cosigner validation is skipped.
  • Otherwise, the cosignature binds the executor to the permit signature hash.

This enables gasless cancellation: withholding the cosignature can prevent further execution.

Cosignature nonces:

  • nonce 0 is reusable until filled/expired or canceled in PermitC
  • non-zero nonces are consumed on first use and cannot be reused

Executor validation hook (onchain authorization)

A permit may include a hook implementing an executor validation interface.

  • If hook == address(0), hook validation is skipped.
  • Otherwise, the handler calls the hook with the additional data hash and full swap context.

This enables onchain authorization policies that need more context than an offchain cosignature.


Solidity: building transferData

The AMM expects transferData to begin with the 32-byte ABI encoding of the handler address.

function encodeTransferData(
address permitTransferHandler,
bytes1 permitType,
bytes memory abiEncodedPermitStruct
) pure returns (bytes memory transferData) {
// transferData := abi.encode(handler) || (permitType || abi.encode(permitStruct))
bytes memory transferExtraData = bytes.concat(permitType, abiEncodedPermitStruct);
return bytes.concat(abi.encode(permitTransferHandler), transferExtraData);
}

Example (fill-or-kill):

bytes memory permitStruct = abi.encode(
FillOrKillPermitTransfer({
permitProcessor: permitC,
from: maker,
nonce: nonce,
permitAmount: permitAmount,
expiration: expiration,
signature: permitSignature,
cosigner: cosigner,
cosignatureExpiration: cosigExp,
cosignature: cosig,
hook: validationHook,
hookData: hookData
})
);

bytes memory transferData = encodeTransferData(
address(PERMIT_TRANSFER_HANDLER),
FILL_OR_KILL_PERMIT,
permitStruct
);

viem: encoding transferData

Below is a minimal viem-style approach for constructing the same bytes layout.

import { encodeAbiParameters, parseAbiParameters, concatHex, padHex, toHex } from 'viem'

export function encodeTransferDataPermit(
handler: `0x${string}`,
permitType: 'fillOrKill' | 'partialFill',
encodedPermitStruct: `0x${string}`,
): `0x${string}` {
// First 32 bytes: abi.encode(address(handler))
const handlerWord = encodeAbiParameters(parseAbiParameters('address'), [handler])

// 1 byte discriminator
const typeByte = permitType === 'fillOrKill' ? '0x00' : '0x01'

// transferExtraData := typeByte || encodedPermitStruct
const transferExtraData = concatHex([typeByte as `0x${string}`, encodedPermitStruct])

// transferData := handlerWord || transferExtraData
return concatHex([handlerWord, transferExtraData])
}

encodedPermitStruct should be the ABI encoding of FillOrKillPermitTransfer or PartialFillPermitTransfer.


Signing permits (PermitC)

PermitTransferHandler uses PermitC-style EIP-712 signatures.

Two permit families are used:

  • Fill-or-kill: PermitTransferFromWithAdditionalData(...)
  • Partial fill: PermitOrderWithAdditionalData(...)

Both incorporate an additional data hash whose type includes a Swap struct (conceptually):

  • partialFill
  • recipient
  • amountSpecified / limitAmount
  • tokenOut
  • exchange fee recipient and BPS
  • cosigner
  • hook

The EIP-712 domain separator comes from the specific PermitC instance (permitProcessor) selected in the permit data.

Practical guidance

  • Prefer building signatures using the PermitC SDK/tools for the chosen PermitC version.
  • Ensure the same fields are used when building the swap additional data hash; the PermitTransferHandler will recompute it and PermitC will validate it.

This documentation intentionally avoids re-specifying the full PermitC type layouts. Treat PermitC as the source of truth for the signing schema.


Integration notes

  • The permit holder (from) may be different from the swap executor. The permit signature authorizes the transfer from from to the AMM.
  • Multi-hop swaps still use a single final net amountIn pull of the route’s overall input token.
  • The handler does not use callbackData (it returns empty callback data).

Limit Break

TwitterLimitBreak.comMedium

© 2026 Limit Break International, Inc. All rights reserved.

Privacy PolicyTerms of ServiceCookie PolicyDo Not Sell My Info