Skip to main content

Understanding and Implementing Bundler3 and Adapters in Solidity

This tutorial walks you through the implementation and usage of Morpho’s Bundler3 contract along with GeneralAdapter1 (one of the adapters taken as example), explaining key concepts and functionalities.

1. Overview

Bundler3 is Morpho’s latest multicall contract, allowing you to batch multiple operations in a single transaction. Unlike simpler multicall implementations, Bundler3 supports callbacks, authorizations, and slippage checks (via adapters), making it more powerful and flexible for advanced DeFi scenarios.

GeneralAdapter1 is a chain-agnostic adapter that integrates Bundler3 with various protocols and token standards, including:

  • ERC20 wrappers
  • ERC4626 vaults
  • Morpho protocol interactions
  • Permit2 transfers
  • Wrapped native token operations

By combining Bundler3 with GeneralAdapter1, you can create seamless one-click flows—e.g., wrapping tokens, supplying to Morpho, repaying debt, or migrating positions—all within a single atomic transaction.


2. Key Components of Bundler3

2.1 Imports and Base Setup

// Solidity

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity 0.8.28;

import { IBundler3, Call } from "./interfaces/IBundler3.sol";
import { ErrorsLib } from "./libraries/ErrorsLib.sol";
import { UtilsLib } from "./libraries/UtilsLib.sol";

/// @custom:security-contact security@morpho.org
/// @notice Enables batching multiple calls in a single transaction.
/// @notice Can be re-entered by the last callee, provided a correct callback hash (for advanced flows).
contract Bundler3 is IBundler3 {
address public transient initiator;
bytes32 public transient reenterHash;

// ...
}

2.2 State Variables

  • initiator: Stores the EOA (externally owned account) or contract that first called multicall.
  • reenterHash: A hash tying together the caller address and the expected calldata for safe reentry.

2.3 Core Functions

  • multicall(Call[] calldata bundle):
    1. Sets the initiator transiently.
    2. Iterates over all calls in bundle.
    3. Resets the initiator to address(0) at the end.
  • reenter(Call[] calldata bundle):
    1. Verifies that the caller + calldata hash matches reenterHash.
    2. Executes _multicall for nested calls (e.g. during a flash loan callback).
  • _multicall(Call[] calldata bundle):
    1. The internal engine that performs each Call in order.
    2. Uses the callbackHash from each Call to set or clear reenterHash.
    3. Respects the skipRevert flag to either continue or revert on call failure.

A single call in the bundle is defined by:

// Solidity

struct Call {
address to; // Contract to call
bytes data; // Calldata
uint256 value; // Ether to send
bool skipRevert; // Whether to ignore revert on this call
bytes32 callbackHash; // Hash for reentrancy checks
}

3. Key Components of GeneralAdapter1

While Bundler3 can directly call any contract, adapters provide safe wrappers around complex flows (e.g. token approval, slippage checks, Morpho logic). A prime example is GeneralAdapter1:

// Solidity

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity 0.8.28;

import { IWNative } from "../interfaces/IWNative.sol";
import { IERC4626 } from ...
import { IMorpho, MarketParams } from ...
import { CoreAdapter } from "./CoreAdapter.sol";

// ...
contract GeneralAdapter1 is CoreAdapter {
IMorpho public immutable MORPHO;
IWNative public immutable WRAPPED_NATIVE;

constructor(address bundler3, address morpho, address wNative) CoreAdapter(bundler3) {
// ...
}

// ... Many helper functions for ERC20, ERC4626, Morpho ...

}

Adapter Highlights

  • ERC20 Wrapping/Unwrapping:

    • erc20WrapperDepositFor / erc20WrapperWithdrawTo
    • Useful if you need to convert from one ERC20 to a wrapper token.
  • ERC4626 Vault Interactions

    • erc4626Mint, erc4626Deposit, erc4626Withdraw, erc4626Redeem
    • Simplifies depositing/withdrawing from yield vaults (e.g. Yearn, Balancer, etc.).
  • Morpho Interactions

    • morphoSupply, morphoBorrow, morphoRepay, morphoWithdraw, morphoFlashLoan
    • Each function safely handles approvals, reentrancy checks, and slippage parameters.
  • Permit2 / ERC20 Transfers

    • permit2TransferFrom, erc20TransferFrom
    • Minimizes transaction overhead by signing a permit instead of requiring explicit user approvals.
  • Wrapped Native Token

    • wrapNative, unwrapNative
    • Automates bridging ETH ↔ WETH (or other chain’s native token wrapper).

All these methods are restricted to calls from Bundler3 only, via the onlyBundler3 modifier, preventing malicious usage or accidental direct approvals.

4. Understanding Bundler3’s Core Features

4.1 Transient Storage & Callbacks

initiator (transient) ensures adapters know the original EOA interacting, so that approvals and ownership checks can be enforced. reenterHash ensures that any nested call (like a flash loan callback) is pre-authorized by the original transaction data. This prevents malicious reentry with altered parameters.

4.2 Atomic Multicalls

Bundler3’s multicall allows you to sequence calls in a single transaction—complete with fallback logic. For example, you can do:

  1. Wrap ETH → WETH
  2. Approve WETH → Morpho
  3. Supply WETH as collateral
  4. Borrow DAI
  5. Swap DAI for USDC
  6. Repay an existing USDC debt

All in one shot, with each step optionally rolling back if an intermediate check fails—unless skipRevert is set to true.

5. Security Considerations

5.1 Reentrancy Protection

Bundler3 uses a reenterHash mechanism to enforce that only the expected callback can reenter. Adapters use onlyBundler3 to block unauthorized direct calls.

5.2 Slippage & Max Amounts

Many adapter functions accept type(uint).max to mean “use the entire balance.” Slippage checks (e.g. require(suppliedAssets.rDivUp(...) <= maxSharePriceE27)) ensure you don’t supply or borrow at an unfavorable rate.

5.3 Approvals

By default, Bundler3 is “unowned” and shouldn’t hold any permanent user approvals. Adapters handle approvals as needed, typically forceApproveing tokens only at the moment of the operation.

5.4 Callback Hash

If you’re building more advanced flows (like a flash loan from Morpho), make sure your callbackHash is set correctly in the call struct. A mismatch will cause the transaction to revert for security reasons.

6. Best Practices

6.1 Validate Inputs Thoroughly

require(receiver != address(0), ErrorsLib.ZeroAddress());
require(assets != 0, ErrorsLib.ZeroAmount());

6.2 Use Slippage Checks

Always set maxSharePriceE27 or minSharePriceE27 to guard against unexpected prices.

6.3 Careful with Call Ordering

If you rely on newly wrapped tokens in a second call, ensure your first call does that wrapping.

6.4 Don’t Expose Bundler3 to Unlimited Approvals

The recommended approach is to pass tokens to an adapter (which then uses the bundler’s initiator context), not to approve Bundler3 permanently.

6.5 Plan for Re-Entrant Calls

If a protocol calls back into your adapter (e.g. after flash-loan funds are delivered), ensure you understand the reenter flow and set the correct callbackHash.

7. Next Steps

Check out additional adapters like EthereumGeneralAdapter1 (handles stETH, wstETH), or ParaswapAdapter for token swaps. Explore the Migration Adapters for seamless position transfers (e.g. from Aave or Compound to Morpho). Integrate advanced flows, such as flash loans, by leveraging the onMorphoFlashLoan callback in GeneralAdapter1. For deeper references, see the Bundler3 repository and examine the unit tests under ./test to see real usage examples.

By understanding these concepts and carefully structuring your calls, you can build efficient and secure DeFi flows using Bundler3 and its adapters.