Authorization
Several SwapCore functions take an onBehalfOf parameter — the address that owns the resulting state change — which is separate from msg.sender, the address that pays for the call. This is the integration hook for bundlers, routers, vault adapters, and wrapper contracts: the end user signs and funds the transaction (perhaps through a bundler), while a contract address takes the action on their behalf.
For every onBehalfOf entry point except buySwap, msg.sender must be pre-authorized by onBehalfOf. The check is a single mapping lookup (isAuthorized) plus a self-path fallback.
Source: SwapCore.sol
The onBehalfOf pattern
onBehalfOf patternInternally, SwapCore runs every guarded call through this helper:
function _isSenderAuthorized(address onBehalfOf) internal view returns (bool) {
return msg.sender == onBehalfOf || isAuthorized[onBehalfOf][msg.sender];
}So the rule is simple: you can always act on your own behalf, and you can delegate that right to any other address by calling setAuthorization(delegate, true). Revoke with setAuthorization(delegate, false).
A failure on this check reverts with E206.
Functions that require authorization
onBehalfOf, or a delegate they've authorized
onBehalfOf, or a delegate they've authorized
onBehalfOf, or a delegate they've authorized
Current swap owner, or a delegate they've authorized
Original swap.userAddress, or a delegate they've authorized
Functions that do NOT require authorization
buySwap— opening a new position is beneficial toonBehalfOf, and the caller is the one paying collateral and fees. Allowing anyone to open a swap for anyone else is safe and useful (bundlers, pre-funded relayers).makePayment,liquidateSwap,updateMarketRateIndex— all permissionless, no per-user auth.Market admin functions (
createMarket,transferMarketOwnership, etc.) — gated onmarketOwner(themarketOwnermodifier), which is a separate mechanism.
Why supplyCollateral requires auth even though it "helps" onBehalfOf
supplyCollateral requires auth even though it "helps" onBehalfOfA dust deposit from an attacker would stamp lpPositions[marketId][victim].lastDepositBlock = block.number, which makes the victim's own withdrawCollateral revert with E413 in the same block. Without the auth check, an attacker could repeatedly front-run a victim's withdrawal and DoS it one block at a time. The auth gate closes that off at zero cost.
What the onBehalfOf → msg.sender split enables
onBehalfOf → msg.sender split enablesBundlers with Permit2. The bundler holds approvals from the end user, pulls tokens from its own balance (funded via permit), and calls
supplyCollateral(marketId, amount, endUser, minShares). Shares go to the end user; the bundler simply forwarded the call.Wrapper NFTs.
SwapPositionWrapperholds actual swap ownership on-chain while issuing an ERC-721 to the end user. On transfer, the wrapper callstransferSwapPosition(swapId, newOwner, wrapperAddress)— it's authorized because it owns the underlying swap.Vault adapters. Morpho vault adapters use the same trick: the adapter is the on-chain LP, the vault's users get accounting via the vault's own share token.
setAuthorization
Who calls: anyone — the caller is authorizing (or revoking) authorized to act on their own behalf. There is no admin path; every account manages its own authorization list.
What it does: sets isAuthorized[msg.sender][authorized] = status. That's it — a single storage write, no external calls, no checks other than ensuring the value actually changes (it doesn't even check that; writes are idempotent).
Parameters:
authorized
The address you're granting or revoking authorization for.
status
true to grant, false to revoke.
State changes / events:
isAuthorized[msg.sender][authorized] = status.Emits
AuthorizationSet(msg.sender, authorized, status).
Reverts: none directly. The function does not validate authorized != address(0) — authorizing the zero address is a no-op since msg.sender == address(0) is unreachable, so it simply produces a dead entry.
Example — authorizing a bundler once, then letting it act repeatedly:
When the bundler is retired or compromised, revoke:
There is no multi-authorization atomic helper; if you want to authorize multiple addresses in one transaction, call setAuthorization for each via a multicall wrapper on your side.
See also: isAuthorized.
isAuthorized
Solidity auto-generates a getter:
Who calls: anyone — UIs, indexers, other contracts that want to know whether they're currently permitted to call guarded functions on behalf of owner.
What it returns: true if owner has previously called setAuthorization(operator, true) and has not since revoked. Returning true from this function is not the same as _isSenderAuthorized returning true — the internal helper also accepts owner == operator as an implicit self-authorization, but the public mapping does not set that bit. A contract checking the mapping directly should reproduce the operator == owner || isAuthorized[owner][operator] logic if it wants to mirror SwapCore's rule.
State: isAuthorized is per-address, not per-market and not per-function. Once you authorize an operator, they can act for you on any market and any guarded function.
See also: setAuthorization.
Last updated