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