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

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

Function
Who the caller can be

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 to onBehalfOf, 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 on marketOwner (the marketOwner modifier), which is a separate mechanism.

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 onBehalfOfmsg.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. SwapPositionWrapper holds actual swap ownership on-chain while issuing an ERC-721 to the end user. On transfer, the wrapper calls transferSwapPosition(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:

Parameter
Meaning

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