Transaction unwrapping
Apps often batch several calls into one transaction with MultiSend. To the Roles Modifier, such a batch looks like one call to the MultiSend contract. The inner calls stay hidden.
A transaction unwrapper solves this. It is an adapter contract that splits a known batch encoding into its inner calls. The Roles Modifier then checks every inner call on its own, against the normal permissions of the role.
Configure an unwrapper
The owner registers an adapter for one target address and one function selector:
roles.setTransactionUnwrapper(
0x9641d764fc13c8B624c04430C7356C1C7C8102e2, // MultiSendCallOnly v1.3.0
bytes4(keccak256("multiSend(bytes)")),
ITransactionUnwrapper(0xB4Cd4bb764C089f20DA18700CE8bc5e49F369efD) // MultiSendUnwrapper
);After this call, a member can send MultiSend batches, and each inner call must pass the role’s permissions. Without the registration, a batch call reverts unless the role has an explicit permission for the MultiSend contract itself.
Deployed adapters (canonical address on all supported chains):
| Adapter | Address | Unwraps |
|---|---|---|
MultiSendUnwrapper | 0xB4Cd4bb764C089f20DA18700CE8bc5e49F369efD | multiSend(bytes) batches |
MorphoBundler3Unwrapper | 0x7533922A155DC6b4Cc0ae4E74D70a73bc86dD3E8 | Morpho Bundler3 multicalls |
The Zodiac App configures the MultiSend unwrapper for you.
The adapter interface
interface ITransactionUnwrapper {
function unwrap(
address to,
uint256 value,
bytes calldata data,
Enum.Operation operation
) external view returns (UnwrappedTransaction[] memory result);
}An adapter must be a pure decoder. It receives the outer call and returns the list of
inner calls, each with its own to, value, data, and operation.
An unwrapper is part of your security boundary. If it decodes a batch differently than the target contract executes it, a member can smuggle calls past the permissions. Only register audited adapters, and pin them to exact target addresses.
When you do not need an unwrapper
Use an unwrapper only when one transaction wraps several independent transactions, each with its own target address.
When one function of one contract encodes several actions in its parameters, the normal
condition tree can reach them. Use AbiEncoded nodes and c.calldataMatches for nested
call data, or a custom condition for complex encodings. Example:
Uniswap V4 batches all actions inside one modifyLiquidities call — integrations scope it
with conditions, without an unwrapper.