Skip to main content

Call Forwarder Operations

The Call Forwarder (CallForwarder.sol) extends the composer's functionality by providing a secure execution environment for complex operations. The main composer first validates calls, then forwards them to the Call Forwarder which can execute external calls.

Overview

The Call Forwarder supports the following operations:

OperationCommand IDDescription
EXT_CALL0x20External calls with safety checks
EXT_TRY_CALL0x21External calls with error handling
TRANSFERS0x40Transfer operations (pull, send, approve)
BRIDGING0x90Cross-chain bridge operations

Security Model

The external calls are facilitated by the call forwarder, while this approach seems to be inefficien, it is necessary to prevent malicious callers to execute bad calldata on the composer (e.g. trying to execute transferFrom from a prior caller that approved the composer), also external calls to permit2 contract are blocked by the composer for the same reason.

Entry Point

function deltaForwardCompose(bytes calldata data) external payable

All operations are encoded in the data parameter and executed atomically.

Hierarchical Calling Structure

The system uses a two-layer architecture for secure external interactions:

Call Architecture Summary

  • Main Composer

    • Issues EXT_CALL operations.
    • Calls the Call Forwarder contract via deltaForwardCompose.
  • Call Forwarder

    • Receives and executes forwarded calls.
    • Supports operations:
      • EXT_CALL - any smart contract call (permit2 target is prohibited and so is the transferFrom selector)
      • EXT_TRY_CALL - any call that will enter a fallback operation in case of an error
      • Token TRANSFERS
      • Cross-chain BRIDGING
    • Provides controlled execution to avoid exposing the Composer directly to malicious calls.
  • External Contracts

    • Arbitrary selectors can be targeted.
    • Ensures flexibility while safeguarding Composer from unsafe external interactions.

Example Structure

// Step 1: Encode operations for Call Forwarder
bytes memory callForwarderOperations = abi.encodePacked(
// Call Forwarder operations here
uint8(ComposerCommands.TRANSFERS),
uint8(TransferIds.SWEEP),
assetAddress,
receiverAddress,
uint8(SweepType.AMOUNT),
uint128(amount)
);

// Step 2: Wrap in EXT_CALL to Call Forwarder
bytes memory composerCall = abi.encodePacked(
uint8(ComposerCommands.EXT_CALL),
callForwarderAddress, // Call Forwarder as target
uint128(0), // No native value needed
uint16(callForwarderOperations.length),
callForwarderOperations // Operations for Call Forwarder
);

// Step 3: Execute on main composer
composer.deltaCompose(composerCall);

EXT_CALL Operation

Execute external calls with built-in safety checks.

Parameters

OffsetLength (bytes)Description
020target address
2016value uint128
362dataLength uint16
38dataLengthdata bytes

Notes

  • value is the native currency amount to attach
  • value has a high bit flag indicating whether to use selfbalance()
  • Security: Main composer prevents calls to Permit2 and transferFrom selector
  • Execution: Reverts on failure with the original error

Encoding Example

bytes memory callForwarderOperations = abi.encodePacked(
uint8(ComposerCommands.EXT_CALL), // Operation for Call Forwarder
targetAddress,
uint128(nativeValue),
uint16(callData.length),
callData
);

bytes memory composerCall = abi.encodePacked(
uint8(ComposerCommands.EXT_CALL), // EXT_CALL to Call Forwarder
callForwarderAddress, // Call Forwarder as target
uint128(0), // No native value for Call Forwarder
uint16(callForwarderOperations.length),
callForwarderOperations
);

EXT_TRY_CALL Operation

Execute external calls with error handling and fallback options.

Parameters

OffsetLength (bytes)Description
020target address
2016value uint128
362dataLength uint16
38dataLengthdata bytes
38+dataLength1catchHandling uint8
39+dataLength2catchDataLength uint16
41+dataLengthcatchDataLengthcatchData bytes

Notes

  • catchHandling: Controls error handling behavior
    • 0: Revert on failure (default)
    • 1: Exit function on failure
    • 2+: Continue execution after catch
  • catchData: Operations to execute if the external call fails

Encoding Example

// With error handling - e.g. sweep tokens back to user on failure
bytes memory catchOperations = abi.encodePacked(
uint8(ComposerCommands.TRANSFERS),
uint8(TransferIds.SWEEP),
address(USDC), // asset to sweep
address(user), // receiver (user who initiated the call)
uint8(SweepType.VALIDATE), // validate balance before sweeping
uint128(0) // amount = 0 means sweep entire balance
);

bytes memory tryCall = abi.encodePacked(
uint8(ComposerCommands.EXT_TRY_CALL),
targetAddress,
uint128(nativeValue),
uint16(callData.length),
callData, // the calldata that should be executed
uint8(1), // catchHandling: exit on failure
uint16(catchOperations.length),
catchOperations // the calldata to execute in case of error
);

TRANSFERS Operations

All transfer operations are supported in the Call Forwarder. See the Transfers documentation for detailed information about:

  • TRANSFER_FROM: Pull funds from caller
  • SWEEP: Send funds from contract
  • APPROVE: Approve token spending
  • UNWRAP_WNATIVE: Unwrap native tokens
  • PERMIT2_TRANSFER_FROM: Permit2 transfers

Usage in Call Forwarder

bytes memory transferOp = abi.encodePacked(
uint8(ComposerCommands.TRANSFERS),
uint8(TransferIds.SWEEP),
assetAddress,
receiverAddress,
uint8(SweepType.AMOUNT),
uint128(amount)
);

BRIDGING Operations

Execute cross-chain bridge operations through supported bridge protocols. See the detailed bridge documentation for comprehensive information about all supported bridge protocols.

Basic Parameters

OffsetLength (bytes)Description
01bridgeId uint8
1Variablebridge-specific parameters

Encoding Example

bytes memory bridgeOp = abi.encodePacked(
uint8(ComposerCommands.BRIDGING),
// Bridge-specific parameters...
);

For detailed information about each bridge protocol, including parameter layouts and encoding examples, see:

Example: 1inch Meta-Aggregation

Here we illustrate the complete two-layer calling structure for a swap from USDC to WETH using 1inch.

Two-Layer Security Flow

Main Composer
↓ EXT_CALL validation
Call Forwarder
↓ deltaForwardCompose validation
↓ TRANSFERS (approve) + EXT_CALL (to 1inch) + TRANSFERS (sweep)
External Contracts (Arbitrary selectors allowed)

Complete Process

  1. Main Composer validates the EXT_CALL (blocks transferFrom/Permit2)
  2. Call Forwarder receives validated call via deltaForwardCompose
  3. Call Forwarder executes operations with additional security:
    • Approve 1inch router (TRANSFERS with security checks)
    • Call 1inch swap (EXT_CALL with security checks)
    • Sweep received WETH (TRANSFERS with security checks)

uint256 swapAmount = 4000.0e6;

// this is the default forwarder address
address callForwarderAddress = 0xfCa1154C643C32638AEe9a43eeE7f377f515c801;

// create the call for the forwarder
// the target can e.g. be the 1inch aggregation router
bytes memory callForwarderCall = abi.encodePacked(
uint8(ComposerCommands.EXT_CALL),
address(oneInchAggregationRouter),
uint128(0), // ERC20 has no value
uint16(data.length),
data
);

// we need to approve the 1inch router on the callForwarder level
// note that the approval is skipped if it was already done in the past
bytes memory approve1inch = abi.encodePacked(
uint8(ComposerCommands.TRANSFERS),
uint8(TransferIds.APPROVE),
address(USDC),
address(oneInchAggregationRouter)
);

// expect to receive 1 WETH
// revert if we receive less
uint256 amountExpected = 1.0e18;

// in case the aggregators does not transfer directly to the user
bytes memory sweepAndCheckSlippage = abi.encodePacked(
uint8(ComposerCommands.TRANSFERS),
uint8(TransferIds.SWEEP),
address(WETH),
address(receiver), // this is the receiver of the WETH
uint8(SweepType.AMOUNT),
amountExpected
);


// combine the operations
callForwarderCall = abi.encodePacked(
approve1inch,
callForwarderCall,
sweepAndCheckSlippage
);


// prepare the composer call
// this executes callForwader.deltaForwardCompose(callForwarderCall)
bytes memory composerCall = abi.encodePacked(
uint8(ComposerCommands.EXT_CALL),
callForwarderAddress, // it is important to use the forwarder on the composer level
uint128(value),
uint16(callForwarderCall.length),
callForwarderCall
);

// we need to make sure that teh callForwarder receives the funds directly
bytes memory transferToForwarder = abi.encodePacked(
uint8(ComposerCommands.TRANSFERS),
uint8(TransferIds.TRANSFER_FROM),
address(USDC),
callForwarderAddress,
uint128(swapAmount)
);

// combine the call to the composer
composerCall = abi.encodePacked(
transferToForwarder,
composerCall
);

// execute the operations
composer.deltaCompose(composerCall);