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)
:- Sets the
initiator
transiently. - Iterates over all calls in
bundle
. - Resets the
initiator
toaddress(0)
at the end.
- Sets the
reenter(Call[] calldata bundle)
:- Verifies that the caller + calldata hash matches
reenterHash
. - Executes
_multicall
for nested calls (e.g. during a flash loan callback).
- Verifies that the caller + calldata hash matches
_multicall(Call[] calldata bundle)
:- The internal engine that performs each
Call
in order. - Uses the
callbackHash
from eachCall
to set or clearreenterHash
. - Respects the
skipRevert
flag to either continue or revert on call failure.
- The internal engine that performs each
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:
- Wrap ETH → WETH
- Approve WETH → Morpho
- Supply WETH as collateral
- Borrow DAI
- Swap DAI for USDC
- 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.