UpshiftDocs

Fee Sharing

Earn a share of every deposit you route into Upshift vaults, using origin codes on the Universal Adapter.

If you route user deposits into Upshift vaults from your own app, you can take a fee on top of those deposits. The Universal Adapter has this built in: Upshift registers an origin code for you, you pass that code on every deposit call, and the adapter sends your fee straight to a wallet you control in the same transaction.

There is nothing to deploy, no wrapper contract to write, and no invoicing. Your fee settles on-chain at deposit time.

How it works

An origin code is a 32-byte identifier. On-chain it maps to two values: a fee in basis points, and the address that collects that fee.

mapping(bytes32 => OriginEntry) public origins;

struct OriginEntry {
    uint256 originFee;          // basis points
    address originFeeCollector; // your wallet
}

When a deposit arrives carrying your code, the adapter:

  1. Takes originFee bps off the incoming deposit amount.
  2. Transfers that amount to your originFeeCollector.
  3. Deposits the remainder into the vault and mints shares to the end user.

The fee comes out of the deposit, so the user receives shares for the net amount. Price this into whatever you show them before they sign.

Passing the all-zero code (bytes32(0)) means no origin fee — that is the default for anyone who has not registered.

Fee limits

ValueOn-chain sourceCurrent
Basis-point denominatorBPS_DENOMINATOR()10000
Maximum origin feeMAX_ORIGIN_FEE_BPS()7000

7000 bps is the contract's hard ceiling, not a recommendation. Agree a realistic number with us — it is visible on-chain and it reduces the user's deposit.

Two fee layers

Each enabled vault also carries an Upshift swap fee (vaultInfo(vault).swapFee, capped by MAX_SWAP_FEE_BPS()), applied on the swap paths. Your origin fee is separate and additive. Read both before you quote a net deposit amount to a user.

Getting a code

addOrigin is owner-only, so codes are registered by Upshift — you cannot self-register. Send us three things:

WhatNotes
A short labele.g. acme-app. Becomes your bytes32 code.
Your fee, in bps100 = 1%. Must be ≤ MAX_ORIGIN_FEE_BPS().
Your collector addressReceives the fee. Must be able to hold arbitrary ERC-20s.

Reach us at support@upshift.finance. Registration is a single transaction on our side.

Once registered we can change your fee or collector (updateOrigin) or retire the code (revokeOrigin) without you shipping anything — your integration keeps passing the same code.

Deriving your code

By convention a code is its label as UTF-8 bytes, right-padded with zeros. Derive it yourself rather than hard-coding a hex blob:

import { stringToHex } from 'viem';

const ORIGIN_CODE = stringToHex('acme-app', { size: 32 });
// 0x61636d652d617070000000000000000000000000000000000000000000000000

With ethers v6:

import { encodeBytes32String } from 'ethers';

const ORIGIN_CODE = encodeBytes32String('acme-app');

Labels are capped at 31 bytes with encodeBytes32String (it reserves a terminator) and 32 bytes with stringToHex. Confirm the exact code with us after registration — the contract stores raw bytes and treats an unregistered code as invalid.

Passing the code

Every deposit entry point on the adapter takes originCode as its first argument:

FunctionUse for
deposit(bytes32 originCode, uint256 depositAmount, address vaultAddr, address assetAddr, address receiverAddr)Depositing the vault's reference asset directly.
depositNativeToken(bytes32 originCode, address vaultAddr, address receiverAddr)Native ETH into a wrapped-native vault. payable.
swapAndDeposit(bytes32 originCode, address vaultAddr, address receiverAddr, SwapParams[] swapParams)Any whitelisted ERC-20, swapped to the reference asset en route.
swapAndDepositNativeToken(bytes32 originCode, address vaultAddr, address receiverAddr)Native ETH into a non-wrapped-native vault. payable.

Each returns the shares minted to receiverAddr.

Via the SDK

The SDK picks the right entry point from the asset you pass, so you call one method for all four paths:

import AugustSDK from '@augustdigital/sdk';
import { stringToHex } from 'viem';

const sdk = new AugustSDK({ appName: '<APP_NAME>' });
sdk.evm.setSigner(signer);

const txHash = await sdk.evm.swapRouterDeposit({
  chainId: 1,
  vault: '0x74ad2f789ed583dbd141bbdafc673fe1f033718b',
  depositAsset: '0xdAC17F958D2ee523a2206206994597C13D831ec7', // USDT
  amount: '1000',
  originCode: stringToHex('acme-app', { size: 32 }),
  slippageBps: 50,
});

swapRouterDeposit reads the vault's reference asset on-chain, chooses the direct / swap / native path, fetches and pins the swap quote, handles the ERC-20 approval, and forwards your originCode on whichever path it takes. Omit originCode and it falls back to the code set on the SDK instance (see below), then to the zero sentinel.

Requires an SDK release newer than 8.24.0. Up to and including 8.24.0, swapRouterDeposit accepts originCode but drops it before dispatch, so deposits settle on the zero code and no fee is collected. On an older version, call swapAndDeposit, depositViaSwapRouter, or depositNativeViaSwapRouter directly instead — those have always forwarded it.

Reading a code's settings

const [originFee, originFeeCollector] = await adapter.origins(ORIGIN_CODE);

An unregistered code returns a zero fee and the zero address.

Setting the code on the SDK instance

Instead of passing originCode on every call, set it once when you construct the SDK:

const sdk = new AugustSDK({
  appName: '<APP_NAME>',
  attribution: {
    originCode: stringToHex('acme-app', { size: 32 }),
    strictOrigin: true,
  },
});

originCode must be the full 32-byte code; a malformed value throws from the constructor. strictOrigin is optional and defaults to false. The config is process-global: the last AugustSDK constructed in a process wins.

A deposit carries, in order of precedence:

  1. The originCode passed on the call (the router methods take one; vaultDeposit does not).
  2. The instance's attribution.originCode.
  3. The zero sentinel — no origin fee.

When vaultDeposit uses the router

With a code configured, vaultDeposit deposits through SwapRouter.deposit carrying the code — a direct deposit, never a swap — only when all of these hold:

  • The vault is on Ethereum mainnet and registered on the router.
  • The vault accepts the deposit asset directly, and the router whitelists it.
  • It is not a permit, native-token, adapter, OVault, Solana, Stellar, or Sui deposit.

Any other deposit takes the vault's native deposit path and the SDK logs a vaultDeposit:attribution-skipped warning with the reason. With strictOrigin: true, a code that is not registered on the router throws AugustValidationError before any transaction instead. With no code configured, nothing changes.

swapRouterDeposit, depositViaSwapRouter, depositNativeViaSwapRouter, and swapAndDeposit also use the instance code on mainnet, after checking that it is registered. An unregistered code passed explicitly on one of these calls throws before any approval.

Approvals

On the router route the spender is the SwapRouter, not the vault. vaultDeposit approves it for you; if you manage approvals yourself, approve the spender that getDepositRoute returns:

const { via, spender } = await sdk.evm.getDepositRoute({ target: vault, depositAsset });

via is 'router' or 'native'; spender is null for a native-token deposit.

Gas

Measured on a mainnet fork, 1,000 USDC into the Core USDC vault. Each token also needs a one-time approval of the router as a new spender.

RouteGasvs direct
Direct vault deposit~102k—
Router, zero-fee code~162k+~60k
Router, 1% code~173k+~71k

Zero-fee codes and operators

A zero-fee code is valid, but the router emits OriginFeeApplied only when it takes a fee. Attribution-only partners on a zero-fee code cannot be reconciled from that event.

The router owner registers codes with addOrigin, in Crest at /swap-router-config. The origin fee is taken from the deposit amount, so a dust deposit below the fee floor reverts with OriginFeeTooLow.

Prerequisites

Fee sharing rides on the adapter, so the adapter's constraints are yours too.

  • Ethereum and HyperEVM only. The Universal Adapter is deployed at 0xAC771209FF2b71EECfF6E85a9AD01db8Ff2618B0 on Ethereum and 0xF47ce4B8df30bc3B3b6cc21Fe1552A2198A4A4F7 on HyperEVM. HyperEVM has no swap route yet, so codes there apply to deposit and depositNativeToken only.
  • The vault must be enabled on the adapter. Check with vaultInfo(vault) — a referenceAsset of the zero address means it is not registered, and deposits revert with InvalidVault. Enabling a vault is a config call on our side; ask us which vaults you need.
  • Deposit tokens must be whitelisted. Both the token in and the token out of a swap leg. Non-whitelisted tokens revert with InputTokenNotWhitelisted / OutputTokenNotWhitelisted.
  • Swap legs are capped at MAX_SWAPS() (currently 9) per call.
  • Deposits only. The deployed adapter handles deposits and swap-then-deposit. Redemptions go through the vault directly — see the vault interface.

Reporting on your fees

Every applied fee emits an event, indexed by origin code, so you can reconcile independently of anything Upshift reports:

event OriginFeeApplied(
    address indexed vaultAddr,
    address indexed assetAddr,
    uint256 consumableAmount,  // deposit amount the fee was taken from
    uint256 feeAmount,         // what your collector received
    bytes32 indexed originCode
);

Filter on originCode for your whole history. OriginAdded, OriginUpdated, and OriginRevoked give you the audit trail of your code's configuration.

The adapter itself emits no deposit event — the vault does. To resolve what a given deposit actually produced (the real post-swap amount, which differs from the pre-trade quote), use the SDK:

const result = await sdk.evm.getSwapRouterDepositResult({
  txHash,
  vault: '0x74ad2f789ed583dbd141bbdafc673fe1f033718b',
});
// result?.amountOut — reference asset that actually reached the vault
// result?.shares    — shares minted to the receiver

Errors

ErrorCause
InvalidOriginThe code is not registered, or was revoked.
OriginAlreadyExistsaddOrigin on a code that already exists — use updateOrigin.
OriginFeeTooHighRequested fee exceeds MAX_ORIGIN_FEE_BPS().
OriginFeeTooLowThe deposit is small enough that the origin fee would round to zero.
InvalidFeeCollectorThe collector address is not usable.
InvalidVaultThe vault is not enabled on the adapter.
InputTokenNotWhitelistedThe deposit token is not whitelisted.
OutputTokenNotWhitelistedA swap leg's output token is not whitelisted.
TooManySwapsMore than MAX_SWAPS() legs in one call.
SlippageErrorA swap leg produced less than its minAmountOut.
ContractIsPausedThe adapter is paused.

The Universal Adapter reference covers the wider function, event, and error surface — note the accuracy caveat at the top of that page, and treat the signatures here as the verified ones for deposits.