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. Note that SwapCore is deployed as a stateless logic module behind the SwapStore proxy — all storage (including the isAuthorized mapping), all protocol-held tokens, and all events live at the SwapStore address. Token approvals and calls like setAuthorization must target the SwapStore proxy address, not the SwapCore implementation.
The onBehalfOf pattern
Internally, 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
Current swap.userAddress (i.e. the buyer, or whoever the swap was most recently transferred to), or a delegate they've authorized
For the swap-scoped functions, authorization is necessary but not sufficient: exitSwapEarly and transferSwapPosition additionally require that onBehalfOf actually owns the swap (swap.userAddress == onBehalfOf), reverting with E203 otherwise. Being someone's delegate lets you act on their positions — it never lets them (or you) touch a swap they don't own.
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). The only restriction:onBehalfOf == address(0)reverts withE103, so a position can never be minted to the zero address.makePayment,liquidateSwap,updateMarketRateIndex— all permissionless, no per-user auth.createMarket— fully permissionless; anyone can create a market pair and becomes itsmarketOwner. Only the market-admin functionstransferMarketOwnership,terminateMarket, andsetMarketLpWhitelistare gated onmarketOwner(themarketOwnermodifier), which is a separate mechanism from per-user authorization.
Why supplyCollateral requires auth even though it "helps" onBehalfOf
A 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
Bundlers 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.
SwapPositionWrapperis custodial: to wrap, the buyer first callssetAuthorization(wrapperAddress, true), thenwrap(swapId)— the wrapper verifies that pre-authorization and callstransferSwapPosition(swapId, wrapperAddress, buyer)as the buyer's delegate, taking over as the swap'suserAddressand minting an ERC-721 to the buyer. From then on, plain ERC-721 transfers of the NFT never touch SwapCore — core ownership stays with the wrapper for the NFT's whole life.unwrap(tokenId)reverses it: the wrapper callstransferSwapPosition(swapId, nftHolder, wrapperAddress)via the self-path (it is the current owner). One consequence of the custody hand-off: while wrapped,isAuthorizedis keyed on the wrapper's address, so any operators the original owner granted viasetAuthorizationlose their rights over the position until it is unwrapped.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. Like all protocol calls, this is sent to the SwapStore proxy address, where the isAuthorized mapping lives.
What it does: sets isAuthorized[msg.sender][authorized] = status — just a single idempotent storage write, no external calls and no checks.
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