UpshiftDocs

Universal Adapter

Full reference for the Universal Adapter (SwapRouter) — the single entry point for atomically swapping any whitelisted token and depositing the proceeds into an Upshift vault.

The Universal Adapter is a standalone periphery contract that lets a caller atomically swap any whitelisted token through any whitelisted DEX aggregator and deposit the proceeds into an Upshift vault, in one transaction. It also accepts plain deposits and native-token deposits.

The contract never holds funds between transactions. Any residual balance found at the start of a call is swept, and any dust left at the end is refunded.

It is deposit-only — redemptions go through the vault itself, documented in the vault interface.

This reference is generated from the verified source of the live deployment (SwapRouter.sol, solc v0.8.26). Every signature, constant, event, and error below is present on-chain.

Deployment

No other chain has a deployment yet.

How it works

Four things must be configured before a deposit can succeed. All four are owner-only, so ask us to set them up rather than expecting to do it yourself.

The vault must be registered. enableVault records the vault's type, its reference asset, its LP token, and the Upshift swap fee that applies to it. Read the result with vaultInfo(vault) — a zero referenceAsset means the vault is not registered and deposits revert with InvalidVault.

Each token must be whitelisted. whitelistedTokens(token) gates every asset that enters or leaves a swap. Only tokens with 6, 8, or 18 decimals can be whitelisted at all. The whitelist is also what keeps the fee collector and origin collectors from being paid in arbitrary tokens.

Each DEX aggregator needs a router path. enableRouter stores two things: the address the adapter approves tokens to (tokenTransferProxies(router), which is the router itself when the aggregator has no separate proxy), and one authorized 4-byte selector.

The selector allowlist is a deliberate safety boundary. The router address is whitelisted but the swap payload is fully caller-controlled, so without it a caller could aim a crafted payload at any function on the whitelisted router — including its approve or transfer — and reach the adapter's own token approvals. Only isAuthorizedSelector(router, selector) pairs are callable.

An origin code is optional. Passing bytes32(0) means no referral fee. A non-zero code must be registered or the call reverts with InvalidOrigin. See Fee Sharing for how partners get one.

Fee order of operations

Two fees exist: the Upshift swap fee, set per vault and paid to feesCollector, and the partner origin fee, set per origin code and paid to that origin's collector. Which apply depends on the entry point:

Entry pointSwap feeOrigin fee
deposit
depositNativeToken
swapAndDeposit✓ per leg✓ per leg
swapAndDepositNativeToken

On the swap paths the swap fee is taken from the swap output first, then the origin fee from what remains, and the vault receives the rest. Both are deducted before shares are minted, so the depositor's shares reflect the net amount.

Constants

NameValueMeaning
BPS_DENOMINATOR10000Basis-point denominator for both fees.
MAX_SWAP_FEE_BPS7000Ceiling on a vault's swap fee.
MAX_ORIGIN_FEE_BPS7000Ceiling on an origin fee.
MAX_SWAPS9Swap legs allowed in one swapAndDeposit.
VAULT_TYPE_ERC46261Vault type: standard ERC-4626.
VAULT_TYPE_TOKENIZED_VAULT_V22Vault type: Tokenized Vault V2 (multi-asset).
NATIVE_TOKEN_ADDRESS0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeENative-token sentinel, per ERC-7528.

Deposit functions

All four are nonReentrant, require the contract to be configured and unpaused, and return the shares minted to receiverAddr.

Every one runs the same common validation first: a non-zero originCode must be registered (InvalidOrigin), receiverAddr must not be zero or the adapter itself (InvalidReceiver), feesCollector must be set (FeeCollectorNotSet), the vault must be registered (InvalidVault), and the vault's reference asset must not be the native sentinel (NativeReferenceAssetNotSupported).

deposit

Deposits a whitelisted ERC-20 into the vault. No swap, so no swap fee.

function deposit(
    bytes32 originCode,
    uint256 depositAmount,
    address vaultAddr,
    address assetAddr,
    address receiverAddr
) external returns (uint256 shares);
NameTypeDescription
originCodebytes32Origin code, or bytes32(0) for none.
depositAmountuint256Amount to pull from msg.sender.
vaultAddraddressThe registered vault.
assetAddraddressThe asset to deposit. Must be whitelisted.
receiverAddraddressReceives the shares.

For an ERC-4626 vault assetAddr must equal the vault's reference asset, otherwise the call reverts with AssetNotAcceptedByVault. A Tokenized Vault V2 accepts any asset the vault itself accepts.

Reverts with ZeroAmount on a zero amount, InputTokenNotWhitelisted on a non-whitelisted asset, and ZeroSharesReceived if the vault mints nothing.

depositNativeToken

Wraps msg.value into the chain's wrapped native token and deposits that. No swap fee applies on this path.

function depositNativeToken(bytes32 originCode, address vaultAddr, address receiverAddr)
    external
    payable
    returns (uint256 shares);

Requires wrappedNativeTokenAddress to be set (WrappedNativeTokenAddressNotDefined) and a non-zero msg.value (ZeroAmount). Reverts with NativeSwapFailed if wrapping does not credit exactly msg.value. Any dust is refunded to msg.sender.

swapAndDeposit

Runs one to MAX_SWAPS atomic swaps and deposits each leg's output into the vault, returning the summed shares.

function swapAndDeposit(
    bytes32 originCode,
    address vaultAddr,
    address receiverAddr,
    SwapParams[] calldata swapParams
) external returns (uint256 shares);
NameTypeDescription
originCodebytes32Origin code, or bytes32(0) for none.
vaultAddraddressThe registered vault.
receiverAddraddressReceives the shares.
swapParamsSwapParams[]One entry per swap leg.

Each leg is validated before anything executes: tokenIn must differ from tokenOut (InvalidPair), amountIn must be non-zero (ZeroAmount), tokenOut must not be the native sentinel (OutputTokenCannotBeNativeToken), payload must be at least 4 bytes (InvalidPayload), the router must have a registered path (InvalidRouter), and both tokens must be whitelisted (InputTokenNotWhitelisted / OutputTokenNotWhitelisted).

Then, per leg: the payload's selector must be authorized for that router (InvalidNotWhitelisted), minAmountOut must be non-zero (MissingSlippageProtection), amountIn is pulled from msg.sender, the router is called with the payload (SwapFailed on revert), and the output is measured by balance delta. Output below minAmountOut reverts with SlippageError; an unchanged input balance reverts with InputTokenNotSwapped.

Because output is measured by balance difference rather than trusted from the router's return value, a payload that routes proceeds elsewhere fails the slippage check rather than succeeding silently.

Reverts with EmptySwapParams on an empty array and TooManySwaps above MAX_SWAPS. Excess input tokens are refunded to msg.sender at the end.

swapAndDepositNativeToken

Wraps msg.value and deposits it. Unlike depositNativeToken, this path charges the vault's swap fee.

function swapAndDepositNativeToken(bytes32 originCode, address vaultAddr, address receiverAddr)
    external
    payable
    returns (uint256 shares);

Same requirements and reverts as depositNativeToken, plus the swap fee deduction.

Read functions

FunctionReturnsDescription
vaultInfo(address vault)(uint256 swapFee, uint8 vaultType, address referenceAsset, address lpTokenAddress)A registered vault's configuration. A zero referenceAsset means unregistered.
origins(bytes32 code)(uint256 originFee, address originFeeCollector)An origin's fee in bps and its collector. Zero collector means unregistered.
whitelistedTokens(address token)boolWhether the token may be used in a swap or deposit.
tokenTransferProxies(address router)addressThe address the adapter approves tokens to for that router. Zero means no path.
isAuthorizedSelector(address router, bytes4 selector)boolWhether that selector may be called on that router.
feesCollector()addressRecipient of Upshift swap fees.
wrappedNativeTokenAddress()addressThe chain's wrapped native token.
isConfigured()boolWhether one-time configuration has run.
isPaused()boolWhether deposits are paused.
owner()addressThe contract owner.

There is no getOrigin function — read the origins mapping directly.

Administrative functions

Every function below is onlyOwner and nonReentrant, and all except configure require the contract to already be configured.

FunctionDescription
configure(address newFeesCollectorAddr, address wrappedNativeTokenAddr)One-time setup. Reverts with AlreadyConfigured if already run.
pause() / unPause()Halt or resume all deposit entry points.
enableVault(address vaultAddr, uint8 vaultType, uint256 swapFee)Register a vault and set its swap fee. Reads the vault's reference asset and LP token; all decimals must be 6, 8, or 18. Overwrites are intentional.
disableVault(address vaultAddr)Deregister a vault.
enableToken(IERC20Metadata token)Whitelist a token. Rejects any token whose decimals are not 6, 8, or 18 (InvalidErc20Token).
disableToken(address tokenAddr)Remove a token from the whitelist.
enableRouter(address routerAddr, address tokenApprovalAddr, bytes4 authorizedSelector)Register a router path and authorize one selector on it.
disableRouter(address routerAddr, bytes4 authorizedSelector)Remove a router path and de-authorize the selector.
addOrigin(bytes32 originCode, uint256 originFee, address originFeeCollector)Register an origin. The code must be non-zero, the collector must not be zero or the adapter, and the fee must not exceed MAX_ORIGIN_FEE_BPS. A zero fee is allowed.
updateOrigin(bytes32 originCode, uint256 originFee, address originFeeCollector)Change an existing origin's fee or collector. Reverts with InvalidOrigin if the code was never registered.
revokeOrigin(bytes32 originCode)Delete an origin. Subsequent deposits carrying it revert with InvalidOrigin.
updateFeesCollector(address newFeesCollectorAddr)Change the Upshift fee recipient. Reverts with FeesCollectorNoOp if unchanged.
transferOwnership(address newOwner)Transfer ownership.

Events

// Lifecycle
event ContractConfigured();
event ContractPaused();
event ContractResumed();
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

// Registry
event VaultEnabled(address indexed vaultAddr);
event VaultDisabled(address indexed vaultAddr);
event TokenEnabled(address indexed tokenAddr);
event TokenDisabled(address indexed tokenAddr);
event RouterPathEnabled(address indexed routerAddr, address indexed tokenApprovalAddr);
event RouterPathDisabled(address indexed routerAddr);
event FeesCollectorUpdated(address indexed newAddr);

// Origins
event OriginAdded(bytes32 indexed originCode, uint256 originFee, address originFeeCollector);
event OriginUpdated(bytes32 indexed originCode, uint256 originFee, address originFeeCollector);
event OriginRevoked(bytes32 indexed originCode);

// Fees
event OriginFeeApplied(
    address indexed vaultAddr,
    address indexed assetAddr,
    uint256 consumableAmount,
    uint256 feeAmount,
    bytes32 indexed originCode
);
event SwapFeeApplied(
    address indexed vaultAddr,
    address indexed tokenOutAddress,
    uint256 swapAmountOut,
    uint256 feeAmount
);

// Residual handling
event ResidualBalanceSwept(address indexed token, address indexed to, uint256 amount);
event ResidualRefund(address indexed token, address indexed to, uint256 amount);

The adapter emits no deposit event of its own — the vault emits that. To resolve what a deposit actually produced, decode the vault's Deposit log, or use the SDK's getSwapRouterDepositResult.

Errors

Configuration and access

ErrorCause
NotConfiguredconfigure has not run.
AlreadyConfiguredconfigure called twice.
ContractIsPausedThe adapter is paused.
ContractNotPausedunPause on an unpaused contract.
OwnerOnlyCaller is not the owner.
OwnerAddressRequiredZero address passed to transferOwnership.
ReentrancyGuardReentrantCallReentrant call.
ZeroAddress / InvalidAddressA required address was zero, or was the adapter itself.
FeeCollectorNotSetfeesCollector is unset.
FeesCollectorNoOpupdateFeesCollector called with the current value.

Vaults and tokens

ErrorCause
InvalidVaultThe vault is not registered, or its reference asset or decimals are unusable.
InvalidVaultTypevaultType is neither 1 nor 2.
InvalidLpTokenDecimalsThe vault's LP token decimals are not 6, 8, or 18.
InvalidErc20TokenToken decimals are not 6, 8, or 18.
TokenAlreadyEnabled / TokenAlreadyDisabledRedundant whitelist change.
AssetNotAcceptedByVaultERC-4626 vault given an asset other than its reference asset.
ZeroSharesReceivedThe vault minted zero shares.
NativeReferenceAssetNotSupportedThe vault's reference asset is the native sentinel.
FailedToAcquireLpTokenAddressStringErrorlpTokenAddress() reverted during enableVault with a string reason.
FailedToAcquireLpTokenAddressErrorCodelpTokenAddress() panicked during enableVault. Carries the panic code.
FailedToAcquireLpTokenAddressLowLevelErrorlpTokenAddress() reverted during enableVault. Carries raw return data.
FailedToAcquireLpTokenDecimalsStringErrordecimals() reverted during enableVault with a string reason.
FailedToAcquireLpTokenDecimalsErrorCodedecimals() panicked during enableVault. Carries the panic code.
FailedToAcquireLpTokenDecimalsLowLevelErrordecimals() reverted during enableVault. Carries raw return data.

Swaps

ErrorCause
EmptySwapParamsNo swap legs supplied.
TooManySwapsMore than MAX_SWAPS legs.
InvalidPairA leg's tokenIn equals its tokenOut.
ZeroAmountA zero deposit amount, msg.value, or leg amountIn.
InvalidPayloadA leg's payload is shorter than 4 bytes.
OutputTokenCannotBeNativeTokenA leg's tokenOut is the native sentinel.
InputTokenNotWhitelistedThe input token is not whitelisted.
OutputTokenNotWhitelistedA leg's output token is not whitelisted.
InvalidRouterThe router has no registered path.
InvalidRouterPathZero or self address passed to enableRouter.
RouterPathAlreadyDisableddisableRouter on a router with no path.
SelectorRequiredZero selector passed to enableRouter.
InvalidNotWhitelistedThe payload's selector is not authorized on that router.
MissingSlippageProtectionA leg's minAmountOut is zero.
SlippageErrorSwap output was below minAmountOut. Carries the token, actual output, and minimum.
InputTokenNotSwappedThe input balance did not change — the router consumed nothing.
SwapFailedThe router call reverted. Carries its return data.
NativeSwapFailedWrapping the native token did not credit msg.value.
WrappedNativeTokenAddressNotDefinedA native path was used with no wrapped native token configured.

Fees and origins

ErrorCause
InvalidOriginA non-zero origin code is not registered or was revoked. Also thrown by addOrigin on a zero code.
OriginAlreadyExistsaddOrigin on an existing code — use updateOrigin.
InvalidFeeCollectorAn origin's collector is zero or the adapter itself.
OriginFeeTooHighThe fee exceeds MAX_ORIGIN_FEE_BPS at registration, or would consume the entire deposit at deposit time.
OriginFeeTooLowThe deposit is small enough that the origin fee would round to zero.
SwapFeeTooHighenableVault given a swap fee above MAX_SWAP_FEE_BPS.
SwapFeeAmountTooLowThe swap output is small enough that the swap fee would round to zero.

NativeTokenTransferFailed is declared on the interface but is not reachable in this deployment.

Structs

struct VaultInfo {
    uint256 swapFee;          // bps, applied on the swap paths
    uint8 vaultType;          // VAULT_TYPE_ERC4626 or VAULT_TYPE_TOKENIZED_VAULT_V2
    address referenceAsset;   // zero means the vault is not registered
    address lpTokenAddress;   // the vault itself for ERC-4626
}

struct SwapParams {
    address tokenIn;
    address tokenOut;
    uint256 amountIn;
    uint256 minAmountOut;     // must be non-zero
    address router;           // must have a registered path
    bytes payload;            // encoded swap call; its selector must be authorized
}

struct OriginEntry {
    uint256 originFee;          // bps
    address originFeeCollector;
}

Integrating

Most integrations should go through the SDK rather than encoding calls by hand — it resolves the vault's reference asset, picks the right entry point, builds and pins the swap quote, and handles approvals. See EVM actions and, for partner fee sharing, Fee Sharing.