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, solcv0.8.26). Every signature, constant, event, and error below is present on-chain.
Deployment
| Chain | Address |
|---|---|
| Ethereum | 0xAC771209FF2b71EECfF6E85a9AD01db8Ff2618B0 |
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 point | Swap fee | Origin 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
| Name | Value | Meaning |
|---|---|---|
BPS_DENOMINATOR | 10000 | Basis-point denominator for both fees. |
MAX_SWAP_FEE_BPS | 7000 | Ceiling on a vault's swap fee. |
MAX_ORIGIN_FEE_BPS | 7000 | Ceiling on an origin fee. |
MAX_SWAPS | 9 | Swap legs allowed in one swapAndDeposit. |
VAULT_TYPE_ERC4626 | 1 | Vault type: standard ERC-4626. |
VAULT_TYPE_TOKENIZED_VAULT_V2 | 2 | Vault type: Tokenized Vault V2 (multi-asset). |
NATIVE_TOKEN_ADDRESS | 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE | Native-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);| Name | Type | Description |
|---|---|---|
originCode | bytes32 | Origin code, or bytes32(0) for none. |
depositAmount | uint256 | Amount to pull from msg.sender. |
vaultAddr | address | The registered vault. |
assetAddr | address | The asset to deposit. Must be whitelisted. |
receiverAddr | address | Receives 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);| Name | Type | Description |
|---|---|---|
originCode | bytes32 | Origin code, or bytes32(0) for none. |
vaultAddr | address | The registered vault. |
receiverAddr | address | Receives the shares. |
swapParams | SwapParams[] | 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
| Function | Returns | Description |
|---|---|---|
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) | bool | Whether the token may be used in a swap or deposit. |
tokenTransferProxies(address router) | address | The address the adapter approves tokens to for that router. Zero means no path. |
isAuthorizedSelector(address router, bytes4 selector) | bool | Whether that selector may be called on that router. |
feesCollector() | address | Recipient of Upshift swap fees. |
wrappedNativeTokenAddress() | address | The chain's wrapped native token. |
isConfigured() | bool | Whether one-time configuration has run. |
isPaused() | bool | Whether deposits are paused. |
owner() | address | The 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.
| Function | Description |
|---|---|
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
| Error | Cause |
|---|---|
NotConfigured | configure has not run. |
AlreadyConfigured | configure called twice. |
ContractIsPaused | The adapter is paused. |
ContractNotPaused | unPause on an unpaused contract. |
OwnerOnly | Caller is not the owner. |
OwnerAddressRequired | Zero address passed to transferOwnership. |
ReentrancyGuardReentrantCall | Reentrant call. |
ZeroAddress / InvalidAddress | A required address was zero, or was the adapter itself. |
FeeCollectorNotSet | feesCollector is unset. |
FeesCollectorNoOp | updateFeesCollector called with the current value. |
Vaults and tokens
| Error | Cause |
|---|---|
InvalidVault | The vault is not registered, or its reference asset or decimals are unusable. |
InvalidVaultType | vaultType is neither 1 nor 2. |
InvalidLpTokenDecimals | The vault's LP token decimals are not 6, 8, or 18. |
InvalidErc20Token | Token decimals are not 6, 8, or 18. |
TokenAlreadyEnabled / TokenAlreadyDisabled | Redundant whitelist change. |
AssetNotAcceptedByVault | ERC-4626 vault given an asset other than its reference asset. |
ZeroSharesReceived | The vault minted zero shares. |
NativeReferenceAssetNotSupported | The vault's reference asset is the native sentinel. |
FailedToAcquireLpTokenAddressStringError | lpTokenAddress() reverted during enableVault with a string reason. |
FailedToAcquireLpTokenAddressErrorCode | lpTokenAddress() panicked during enableVault. Carries the panic code. |
FailedToAcquireLpTokenAddressLowLevelError | lpTokenAddress() reverted during enableVault. Carries raw return data. |
FailedToAcquireLpTokenDecimalsStringError | decimals() reverted during enableVault with a string reason. |
FailedToAcquireLpTokenDecimalsErrorCode | decimals() panicked during enableVault. Carries the panic code. |
FailedToAcquireLpTokenDecimalsLowLevelError | decimals() reverted during enableVault. Carries raw return data. |
Swaps
| Error | Cause |
|---|---|
EmptySwapParams | No swap legs supplied. |
TooManySwaps | More than MAX_SWAPS legs. |
InvalidPair | A leg's tokenIn equals its tokenOut. |
ZeroAmount | A zero deposit amount, msg.value, or leg amountIn. |
InvalidPayload | A leg's payload is shorter than 4 bytes. |
OutputTokenCannotBeNativeToken | A leg's tokenOut is the native sentinel. |
InputTokenNotWhitelisted | The input token is not whitelisted. |
OutputTokenNotWhitelisted | A leg's output token is not whitelisted. |
InvalidRouter | The router has no registered path. |
InvalidRouterPath | Zero or self address passed to enableRouter. |
RouterPathAlreadyDisabled | disableRouter on a router with no path. |
SelectorRequired | Zero selector passed to enableRouter. |
InvalidNotWhitelisted | The payload's selector is not authorized on that router. |
MissingSlippageProtection | A leg's minAmountOut is zero. |
SlippageError | Swap output was below minAmountOut. Carries the token, actual output, and minimum. |
InputTokenNotSwapped | The input balance did not change — the router consumed nothing. |
SwapFailed | The router call reverted. Carries its return data. |
NativeSwapFailed | Wrapping the native token did not credit msg.value. |
WrappedNativeTokenAddressNotDefined | A native path was used with no wrapped native token configured. |
Fees and origins
| Error | Cause |
|---|---|
InvalidOrigin | A non-zero origin code is not registered or was revoked. Also thrown by addOrigin on a zero code. |
OriginAlreadyExists | addOrigin on an existing code — use updateOrigin. |
InvalidFeeCollector | An origin's collector is zero or the adapter itself. |
OriginFeeTooHigh | The fee exceeds MAX_ORIGIN_FEE_BPS at registration, or would consume the entire deposit at deposit time. |
OriginFeeTooLow | The deposit is small enough that the origin fee would round to zero. |
SwapFeeTooHigh | enableVault given a swap fee above MAX_SWAP_FEE_BPS. |
SwapFeeAmountTooLow | The 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.