# Returns token balance history for a given address. Source: https://docs.yieldpoint.io/api-reference/address/returns-token-balance-history-for-a-given-address https://api.yieldpoint.io/docs/json get /v1/address/{address}/portfolio # Returns ecosystem-level data for UTY and yUTY tokens Source: https://docs.yieldpoint.io/api-reference/ecosystem/returns-ecosystem-level-data-for-uty-and-yuty-tokens https://api.yieldpoint.io/docs/json get /v1 # Get health Source: https://docs.yieldpoint.io/api-reference/health/get-health https://api.yieldpoint.io/docs/json get /health # Returns a list of points per user Source: https://docs.yieldpoint.io/api-reference/leaderboard/returns-a-list-of-points-per-user https://api.yieldpoint.io/docs/json get /v1/leaderboard # Returns a list of points per user including given address Source: https://docs.yieldpoint.io/api-reference/leaderboard/returns-a-list-of-points-per-user-including-given-address https://api.yieldpoint.io/docs/json get /v1/leaderboard/{address} # Returns multi-chain transaction history for a given wallet address. Source: https://docs.yieldpoint.io/api-reference/transactions/returns-multi-chain-transaction-history-for-a-given-wallet-address https://api.yieldpoint.io/docs/json get /v1/transactions/{address} Returns up to 500 lifecycle rows ordered newest-first. Each request targets a single chain via the required `chainId` query parameter: `8453` (Base, hub), `43114` (Avalanche, spoke), or `747474` (Katana, spoke). The query parameter filters by `requestedOnChainId` — the chain where the activity was initiated. Three `kind` values: `mint` (USDC → UTY), `deposit` (UTY → yUTY), and `withdrawal`. yUTY withdrawals follow a cooldown → claimable → claimed lifecycle; UTY withdrawals are born at `claimed` (instant path). Each row carries both `requestedOnChainId` (where the activity was initiated) and `claimedOnChainId` (where the claim was settled — the hub for "claim all on Base", the spoke for OFT bridge-back). These can differ: a request initiated on Avalanche can be claimed on Base, or bridged back to Katana. Calling per chain is the canonical way to render a multi-chain history; the API does not aggregate across chains in a single response. # Returns UTY total supply Source: https://docs.yieldpoint.io/api-reference/uty/returns-uty-total-supply https://api.yieldpoint.io/docs/json get /v1/uty # Returns yUTY APY Source: https://docs.yieldpoint.io/api-reference/yuty/returns-yuty-apy https://api.yieldpoint.io/docs/json get /v1/yuty # User guides Source: https://docs.yieldpoint.io/guides/coming-soon Step-by-step guides for depositing, redeeming, withdrawing, claiming, and bridging UTY and yUTY. User guides for depositing, redeeming, withdrawing, claiming, and bridging are being written. Until they land, the [Protocol](/protocol/architecture/overview) section covers how YieldPoint works at the contract level, and the [App](https://app.yieldpoint.io) is the place to actually use the protocol. In the meantime: * **App:** [app.yieldpoint.io](https://app.yieldpoint.io) * **Community:** [Discord](https://discord.gg/fdxe4efA2M) * **Announcements:** [@YieldPointCap on X](https://x.com/YieldPointCap) # Contracts Source: https://docs.yieldpoint.io/protocol/architecture/contracts Deployed YieldPoint contract addresses, plus the contract layer organization and inheritance chains. YieldPoint deploys on Base (hub) and two spoke chains (Avalanche and Katana). Contract state lives on the hub; spoke chains hold tokens and interact with the hub via LayerZero messages. This page lists the deployed addresses for each chain, then walks through the contract architecture: layer organization, vault inheritance, the LayerZero integration, and a reference table of the key contracts. ## Deployed addresses The `UTY` contract on both Avalanche and Katana uses the same address as `UTY` on Base. Similarly for `yUTY`. This is intentional — YieldPoint used `CREATE3` to produce identical addresses across EVM chains for contracts deployed with the same deployer and salt, so you can hard-code one address per token and route by chain ID. ### Base (hub) * **Chain ID:** `8453` * **LayerZero EID:** `30184` | Contract | Address | Notes | | ---------------------------------- | -------------------------------------------- | ----------------------------------------------------------------------------------------- | | `UTY vault` | `0xBA515304d8153c4b162dC79f867E152DF9c127eb` | ERC-7540 async vault. Deposit USDC to mint UTY 1:1. | | `UTY adapter` (`ShareOFTAdapter`) | `0xC6fE7C5010621Ab3CA5F4C018fAd632f78b4D3f1` | Lockbox adapter for cross-chain UTY transfers. | | `yUTY vault` | `0xBa515EEd0119aCB7CFE8fAb3ACD6b362f3ed5319` | ERC-7540 async vault for yUTY shares. | | `yUTY adapter` (`ShareOFTAdapter`) | `0xCfD564bbf4e50A248835aCe8Ca889f73217162d4` | Lockbox adapter for cross-chain yUTY transfers. | | `yUTY composer` | `0x2eF787561f3A79371Ee1165B7BeE6dc6ffB09832` | Hub-side cross-chain message handler for yUTY deposits and redemptions from spoke chains. | | `USDC` (external) | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` | USDC on Base. The underlying asset for UTY. | Base is the hub chain — all vault state, share price, and withdrawal queue management lives here. UTY is minted and redeemed directly on Base; cross-chain UTY operations go through the adapter. yUTY is minted and redeemed on Base (either directly or via cross-chain composer calls from spokes). ### Avalanche (spoke) * **Chain ID:** `43114` * **LayerZero EID:** `30106` | Contract | Address | Notes | | ------------------------- | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `UTY OFT` | `0xBA515304d8153c4b162dC79f867E152DF9c127eb` | Mint/burn OFT on Avalanche. Same address as the Base UTY vault via CREATE3. | | `yUTY OFT` | `0xBa515EEd0119aCB7CFE8fAb3ACD6b362f3ed5319` | Mint/burn OFT on Avalanche. Same address as the Base yUTY vault via CREATE3. | | `yUTY VaultInterface` | `0x110d9c781D30A99f61154b5d696772AcC941a7FC` | Spoke-side proxy for cross-chain yUTY deposits and redemptions. Charges flat fees on cross-chain calls. | | `yUTY composer` (hub ref) | `0x2eF787561f3A79371Ee1165B7BeE6dc6ffB09832` | LayerZero peer reference — same address as the hub composer on Base. Not deployed on Avalanche; listed as the cross-chain routing target. | #### Avalanche flat fees | Fee | Value | Raw uint256 (18 decimals) | | ---------------------- | ----------- | ------------------------- | | `depositFlatFee` (UTY) | `0.14 UTY` | `140000000000000000` | | `redeemFlatFee` (yUTY) | `0.21 yUTY` | `210000000000000000` | Fees are charged by the spoke `yUTY VaultInterface` on cross-chain deposit and redeem calls. They cover the per-message LayerZero gas cost for spoke-to-hub and hub-to-spoke routing. The fees are held as token balances in the `VaultInterface` contract and swept periodically by the operations role. ### Katana (spoke) * **Chain ID:** `747474` * **LayerZero EID:** `30375` | Contract | Address | Notes | | ------------------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `UTY OFT` | `0xBA515304d8153c4b162dC79f867E152DF9c127eb` | Mint/burn OFT on Katana. Same address as the Base UTY vault via CREATE3. | | `yUTY OFT` | `0xBa515EEd0119aCB7CFE8fAb3ACD6b362f3ed5319` | Mint/burn OFT on Katana. Same address as the Base yUTY vault via CREATE3. | | `yUTY VaultInterface` | `0x4e1a5a4279241f197b307d69bC23fc5aE2461caC` | Spoke-side proxy for cross-chain yUTY deposits and redemptions. Charges flat fees on cross-chain calls. | | `yUTY composer` (hub ref) | `0x2eF787561f3A79371Ee1165B7BeE6dc6ffB09832` | LayerZero peer reference — same address as the hub composer on Base. Not deployed on Katana; listed as the cross-chain routing target. | #### Katana flat fees | Fee | Value | Raw uint256 (18 decimals) | | ---------------------- | ----------- | ------------------------- | | `depositFlatFee` (UTY) | `0.14 UTY` | `140000000000000000` | | `redeemFlatFee` (yUTY) | `0.21 yUTY` | `210000000000000000` | Fees are charged by the spoke `yUTY VaultInterface` on cross-chain deposit and redeem calls, same pattern as Avalanche. *Addresses sourced from the YieldPoint contracts repo at commit `730f599e` on 2026-04-07. Re-sync when the contracts repo deploys new addresses.* ## Architectural layers The YieldPoint contracts are organized into four architectural layers plus a LayerZero integration column. Each layer depends on the layer below it: Examples extend Extensions, which extend Core, which implements Interfaces. The LayerZero column sits in parallel with its own internal structure. The layers are listed in dependency order — top to bottom matches the dependency direction. Test mocks are not part of the runtime dependency graph and are omitted from this view. ## Vault inheritance ### UTYAsyncVaultV1 (abstract base) The base vault inherits from seven upgradeable OpenZeppelin and YieldPoint contracts: * `ERC4626Upgradeable` * `ERC20AuthorizationUpgradeable` (EIP-3009) * `Ownable2StepUpgradeable` * `AccessControlEnumerableUpgradeable` * `UUPSUpgradeable` * `ReentrancyGuardUpgradeable` * `PausableUpgradeable` ### UTY vault configuration `UTY` extends `UTYAsyncVaultV1` with two extensions plus an override: * `UTYAsyncVaultV1Custodian` — adds the custodian sweep, `totalManagedAssets` tracking, `totalAssets()` override, and emergency write-down via `reduceTotalManagedAssets()` * `UTYAsyncVaultV1Instant` — adds the instant redemption path below a configurable threshold * `donate()` is overridden to revert **Why `donate()` reverts for UTY.** The UTY vault maintains a strict 1:1 peg with USDC. In a standard ERC-4626 vault, anyone can call `donate()` to add assets without minting shares, which increases the exchange rate for all shareholders. For UTY, this would break the 1:1 guarantee — if someone donated 1000 USDC, existing UTY holders would suddenly have shares worth more than 1 USDC each. By reverting on `donate()`, the UTY vault ensures the exchange rate can never deviate from 1.0. This is intentional and critical to UTY's design as a stablecoin, not a yield-bearing token. (The yUTY vault, by contrast, allows donations — this increases the exchange rate for existing shareholders, which is the desired behavior for a yield vault.) ### yUTY vault configuration `yUTY` extends `UTYAsyncVaultV1` directly, without the custodian or instant extensions: * Async ERC-7540 vault without the `UTYAsyncVaultV1Instant` extension * 18-decimal shares (no decimals offset; inflation defense via seed deposit) ## LayerZero contracts The LayerZero integration layer splits between hub-side and spoke-side contracts. ### Hub-side (Base) * `ShareOFTAdapter` — OFT adapter wrapping the vault's share token. Uses the lockbox model: tokens sent cross-chain are locked in the adapter, not burned. Inherits `PausableWithPauserAuth` for bridge-level pause control. * `UTYVaultComposer` — handles incoming cross-chain `lzCompose` messages, executes vault operations on behalf of the caller, and manages refund recovery for failed composes. Extends `VaultComposerSyncPatched` (the YieldPoint patched version of the LayerZero base composer) and `Ownable2Step`. ### Spoke-side (Avalanche, Katana) * `OFTExtended` — the UTY and yUTY token representation on spoke chains. Mint/burn model: tokens arriving from the hub are minted, tokens leaving are burned. Extends `OFTUpgradeable`, `ERC20AuthorizationUpgradeable`, and `Ownable2StepUpgradeable`. * `UTYVaultInterface` — the spoke-chain proxy that makes yUTY deposits and redemptions feel local to a user on Avalanche or Katana. Charges flat fees on cross-chain calls (see [Tokens and fees](/protocol/architecture/tokens-and-fees)). Uses `AccessControl`, ERC-7201 namespaced storage, and maintains a per-user pending-claims counter that prevents double-spending of claim credits. ## Key contracts | Contract | Purpose | Deployment | | ------------------------------- | ----------------------------------------------------------------------- | -------------- | | `UTYAsyncVaultV1` | Base vault implementation (abstract) | Hub only | | `UTYAsyncVaultV1Custodian` | Custodian sweep + `fundRedemptions` extension | Hub only | | `UTYAsyncVaultV1Instant` | Instant redemption extension | Hub only (UTY) | | `PausableWithPauserAuth` | Bridge-level pause (ERC-7201 namespaced) | Hub only | | `ERC20AuthorizationUpgradeable` | EIP-3009 implementation | All chains | | `UTYVaultComposer` | Cross-chain message handler + refund recovery | Hub only | | `UTYVaultInterface` | Spoke vault proxy + access control + flat fees + pending-claims counter | Spoke chains | | `OFTExtended` | Extended OFT token (UTY/yUTY on spokes) | Spoke chains | | `ShareOFTAdapter` | Hub OFT lockbox + bridge-level pause | Hub only | ## Example contracts YieldPoint ships two concrete vault implementations ready for deployment: | Contract | Description | Extends | | -------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------- | | `UTY` | Unity stablecoin vault (1:1 USDC peg, custodian sweep) | `UTYAsyncVaultV1`, `UTYAsyncVaultV1Instant`, `UTYAsyncVaultV1Custodian` | | `yUTY` | Yield-bearing vault shares for UTY (async ERC-7540, no instant extension) | `UTYAsyncVaultV1` | Both contracts hardcode their names and symbols. Bonding period, deposit caps, and other parameters are configured per vault after deployment. # Core flows Source: https://docs.yieldpoint.io/protocol/architecture/flows How deposits, redemptions, and cross-chain operations route through the YieldPoint contracts. YieldPoint has five core operation flows: UTY deposit (direct on Base), yUTY deposit from a spoke chain, UTY redemption, yUTY redemption, and cross-chain redemption. This page walks through each one. All cross-chain flows show the spoke column generically as "Spoke chain" — the flow is identical whether the user is on Avalanche or Katana. ## UTY deposit The simplest flow. You deposit USDC on Base, the UTY vault mints UTY 1:1, and the USDC is swept to the custodian wallet. ```mermaid theme={"dark"} sequenceDiagram participant User participant Vault as UTY vault participant Custodian User->>Vault: deposit(USDC) Vault->>User: mint UTY 1:1 Vault->>Custodian: sweep USDC ``` You call `deposit(assets, receiver)` on the UTY vault with an equal amount of USDC pre-approved. The vault mints UTY 1:1 against the deposited USDC. No share price math — UTY maintains a strict 1:1 peg with USDC. The vault transfers the deposited USDC to the custodian wallet immediately. This is the custodian extension (`UTYAsyncVaultV1Custodian`) that tracks `totalManagedAssets` for the off-chain-held portion of the backing. ## yUTY deposit (cross-chain) From a spoke chain, you deposit UTY into the yUTY vault. The call routes through the spoke `VaultInterface`, over LayerZero to the hub composer, into the vault, and the resulting yUTY shares bridge back to your address on the spoke. ```mermaid theme={"dark"} sequenceDiagram participant User as User (Spoke) participant VI as VaultInterface (Spoke) participant OFT as UTY OFT (Spoke) participant Composer as Composer (Hub) participant Vault as yUTY vault (Hub) participant Adapter as yUTY ShareOFTAdapter (Hub) User->>VI: deposit(UTY) VI->>OFT: forward (minus flat fee) OFT->>Composer: lz message Composer->>Vault: deposit on user's behalf Vault->>Adapter: mint and lock yUTY shares Adapter-->>User: lz return (yUTY OFT minted on spoke) ``` You call `deposit(assets, receiver)` on the spoke `yUTY VaultInterface` with an equal amount of UTY pre-approved. The interface deducts its flat deposit fee and forwards the remaining UTY cross-chain. The UTY OFT relays the message via LayerZero to the hub composer. The composer's `lzCompose` handler calls `deposit` on the yUTY vault on your behalf. The yUTY shares minted by the vault are locked in the `yUTY ShareOFTAdapter` and a LayerZero message mints the equivalent yUTY OFT supply to your address on the spoke chain. **There is no cross-chain UTY deposit flow.** UTY can only be minted on Base by depositing USDC directly to the UTY vault. Once minted, UTY can be bridged to spoke chains and used to mint yUTY cross-chain via the flow above. ## UTY redemption UTY redemption uses a 7-day async bonding path. You call `requestRedeem` on Base, the vault burns your UTY and creates a `WithdrawalRequest` with `unlockTime = now + 7 days`. Once the bonding period elapses, you call `redeemById(requestId)` to receive USDC from the vault's on-chain buffer. ```mermaid theme={"dark"} sequenceDiagram participant User participant Vault as UTY vault participant Req as WithdrawalRequest Note over User,Req: Day 0: request User->>Vault: requestRedeem(shares) Vault->>Vault: burn UTY, lock rate Vault->>Req: create request, unlockTime = now + 7d Note over User,Req: Wait 7 days Note over User,Req: Day 7+: claim User->>Vault: redeemById(requestId) Vault->>Vault: check unlock, transfer USDC Vault->>User: transfer USDC ``` **UTY redemption is Base-only.** To redeem UTY for USDC you need to be on Base. If you hold UTY on a spoke chain, bridge it to Base first, then call `requestRedeem` on the hub vault. ## yUTY redemption yUTY redemption is instant on the hub chain. You call `requestRedeem`, the vault burns your shares and creates a `WithdrawalRequest` with no bonding delay. The matching `redeemById(requestId)` call is available in the same block and transfers the underlying UTY to you. ```mermaid theme={"dark"} sequenceDiagram participant User participant Vault as yUTY vault participant Req as WithdrawalRequest User->>Vault: requestRedeem(shares) Vault->>Vault: burn shares, lock rate Vault->>Req: create request, unlockTime = now User->>Vault: redeemById(requestId) Vault->>Vault: transfer assets Vault->>User: transfer UTY ``` **ERC-7540 flow, no bonding delay.** yUTY still follows the ERC-7540 async pattern with separate request and claim calls, so off-chain tooling built against the standard works unchanged. The bonding period is a vault configuration; it's currently set to 0, which makes the claim call available immediately. When shares are burned at request time, the exchange rate is locked — any yield accrual between request and claim accrues to remaining shareholders. ## Cross-chain redemption From a spoke chain, requesting a yUTY redemption is a two-phase flow. The request sends the redemption cross-chain to the hub, where shares are burned. The claim is a separate spoke-originated transaction that instructs the hub composer to settle the withdrawal request and bridge UTY back to the user on the spoke. The request phase looks like this: ```mermaid theme={"dark"} sequenceDiagram participant User as User (Spoke) participant VI as VaultInterface (Spoke) participant Composer as Composer (Hub) participant Vault as yUTY vault (Hub) User->>VI: requestRedeem(yUTY) VI->>Composer: lz message Composer->>Vault: requestRedeem on user's behalf Vault->>Vault: burn shares, lock rate, create WithdrawalRequest Note over User,Vault: No return message — user watches hub state via indexer or Base RPC ``` Once the hub-side request confirms (typically 10–60 seconds via LayerZero), the claim phase routes back through the spoke interface and composer: ```mermaid theme={"dark"} sequenceDiagram participant User as User (Spoke) participant VI as VaultInterface (Spoke) participant Composer as Composer (Hub) participant Vault as yUTY vault (Hub) User->>VI: redeem() VI->>VI: check pending-claims counter, decrement VI->>Composer: lz message Composer->>Vault: redeem as operator (COMPOSER_ROLE) Vault-->>User: lz return (UTY OFT minted on spoke) ``` The composer holds the `COMPOSER_ROLE` on the vault, which allows it to act as an operator for any user's claim. The spoke `VaultInterface` maintains a per-user pending-claims counter that prevents double-spending of claim credits. # Overview Source: https://docs.yieldpoint.io/protocol/architecture/overview How the YieldPoint protocol is structured: hub-spoke model, ERC-7540 vaults, and LayerZero messaging. This section covers the YieldPoint protocol architecture for integrators and protocol developers building on UTY and yUTY. You'll find the contract layout, the token model, fee economics, state distribution, and the core operation flows. ## Design principles YieldPoint is built around four design principles that shape every contract and flow: 1. **Single source of truth.** All vault state lives on Base. Spoke chains hold only token balances and allowances — the familiar ERC-20 surface. 2. **Standard interfaces.** ERC-7540 compliance on the hub enables DeFi integrations and asynchronous withdrawals out of the box. 3. **Gas abstraction.** You transact on your preferred chain. The protocol handles cross-chain messaging and pays the hub-chain (Base) gas when routing back to a spoke. 4. **Minimal trust assumptions.** Cross-chain operations rely on LayerZero's security model. No custom bridge, no additional trust layer. ## Mental model Base is the hub. All vault state — share price, withdrawal queue, managed assets — lives there. Avalanche and Katana are spoke chains: you hold UTY and yUTY as ERC-20 tokens and trigger cross-chain operations from spokes. LayerZero carries the messages between them. Whether you deposit on Base directly or from a spoke chain, the vault on Base is the thing that actually mints or redeems shares. ## Explore the architecture How deposits, redemptions, and cross-chain operations route through the contracts. Where protocol state lives across hub and spoke chains, and how operations route. Deployed addresses for each chain, plus how the contracts are organized by layer and how they compose. How UTY and yUTY move across chains as OFTs, and the flat fee model on spoke chains. # State distribution Source: https://docs.yieldpoint.io/protocol/architecture/state Where YieldPoint protocol state lives across hub and spoke chains, and how operations route. All YieldPoint vault state lives on Base (the hub). Spoke chains only hold token balances and allowances — the familiar ERC-20 surface. This page lists exactly which data lives where, and which operations execute on which chain. ## Hub vs spoke state Local ERC-20 state (balances, allowances) lives on each spoke chain because the OFT contracts track it there. Everything else — supply, share price, withdrawal queue — lives on the hub because those values are global to the protocol and must be consistent regardless of which chain you query from. | Data | Location | Rationale | | ---------------------------------------- | -------- | --------------------------------------------------------------------------- | | `balanceOf(user)` | Spoke | OFT tracks local balances per chain. | | `allowance(owner, spender)` | Spoke | ERC-20 allowances are local. | | `totalSupply()` | Hub | Total minted supply (includes tokens locked in the OFT adapter on the hub). | | `totalAssets()` | Hub | Vault's total managed assets, including custodian-held UTY. | | `convertToAssets(shares)` | Hub | Requires the global share price, which is a hub-side computation. | | `convertToShares(assets)` | Hub | Requires the global share price. | | `previewDeposit(assets)` | Hub | Vault math lives on the hub. | | `previewRedeem(shares)` | Hub | Vault math lives on the hub. | | `maxDeposit(receiver)` | Hub | Vault limits are hub state. | | `maxRedeem(owner)` | Hub | Vault limits are hub state. | | `pendingRedeemRequest(id, controller)` | Hub | Withdrawal queue state. | | `claimableRedeemRequest(id, controller)` | Hub | Withdrawal queue state. | | `getWithdrawalRequests(controller)` | Hub | User's pending requests. | ## Operation routing Reading a balance or approving a spender is a local call on whatever chain you're on. Depositing, redeeming, or requesting a redemption routes to the hub — either directly (if you're on Base) or via the spoke `VaultInterface` and the LayerZero composer (if you're on a spoke). | Operation | Type | Executes on | Notes | | ---------------------------------------- | ----- | ----------- | -------------------------------------------------------------------------------------- | | `balanceOf(user)` | Read | Spoke | Local OFT balance. | | `approve(spender, amount)` | Write | Spoke | Local ERC-20 allowance. | | `transfer(to, amount)` | Write | Spoke | OFT handles cross-chain if needed. | | `deposit(assets, receiver)` (UTY) | Write | Hub | Base only — no cross-chain UTY minting. | | `deposit(assets, receiver)` (yUTY) | Write | Hub | Via spoke `VaultInterface` → composer when called from a spoke chain. | | `requestRedeem` / `redeem` (UTY) | Write | Hub | Base only — no cross-chain UTY redemption. | | `requestRedeem` / `redeem` (yUTY) | Write | Hub | Via spoke `VaultInterface` → composer when called from a spoke chain. | | `redeemById(requestId, receiver)` | Write | Hub | Direct claim by withdrawal request ID. | | `redeem(shares, receiver, controller)` | Write | Hub | Claim across the controller's matured requests in storage order. | | `withdraw(assets, receiver, controller)` | Write | Hub | Claim a target asset amount across the controller's matured requests in storage order. | # Tokens and fees Source: https://docs.yieldpoint.io/protocol/architecture/tokens-and-fees How UTY and yUTY work as Omnichain Fungible Tokens on the hub and spoke chains, and the cross-chain fee economics. This page covers how UTY and yUTY tokens move between chains (the OFT pattern) and the flat fees that the spoke-chain `UTYVaultInterface` charges to cover LayerZero messaging costs. ## OFT pattern UTY and yUTY implement LayerZero's Omnichain Fungible Token (OFT) pattern. The token contract on the hub chain is different from the token contract on spoke chains — different roles, different storage, different responsibilities. * The vault contract IS the token. `UTY` and `yUTY` are both ERC-7540 vaults that extend ERC-20, so the share token is the vault itself. * `ShareOFTAdapter` wraps the vault token for cross-chain transfers using the **lockbox model**: tokens sent cross-chain are locked in the adapter, not burned. * Redeeming a cross-chain transfer releases the locked tokens back to the recipient. * `OFTExtended` represents the token on each spoke chain. No vault logic — just a fungible token. * Uses the **mint/burn model**: tokens arriving from the hub are minted on the spoke; tokens leaving the spoke are burned. * Spoke-chain balances are local. The global supply is the sum of the hub-side vault's `totalSupply()` and the spoke-side mint/burn balances. **Why spoke-only deployment matters for fee collection.** The `UTYVaultInterface` on spoke chains collects flat fees by holding a small token balance and sweeping it via `balanceOf(address(this))`. This is only safe with mint/burn OFTs, where the only tokens sitting in the contract are collected fees. If the interface were deployed alongside a lockbox adapter (which holds locked user funds), user deposits would be indistinguishable from fee balances and could be swept by the owner. The `UTYVaultInterface` must never be deployed on a chain that uses OFT adapters. The `initialize()` function enforces this by checking `approvalRequired()` on both OFTs — OFT adapters return `true` (lockbox needs approval), native OFTs return `false` (mint/burn). If either check returns `true`, initialization reverts with `OFTAdapterNotSupported()`. The model is different across layers but the user experience is uniform: a user on Avalanche holds UTY in a single wallet address and transfers it like any ERC-20, regardless of whether the underlying is a lockbox-locked adapter token (on the hub) or a locally-minted OFT (on a spoke). ```mermaid theme={"dark"} flowchart LR subgraph Hub["Hub (Base)"] Vault[yUTY vault] Lockbox[ShareOFTAdapter] Vault --> Lockbox end subgraph Spoke["Spoke chain"] OFT[OFTExtended: mint/burn] end Lockbox -- lz --> OFT ``` Hub uses lock/unlock; spoke uses mint/burn. LayerZero messages carry the token movement between them. ## EIP-3009 support All YieldPoint tokens — hub vaults (`UTY`, `yUTY`) and spoke OFTs (`OFTExtended`) — support EIP-3009's `transferWithAuthorization`. This enables signature-based transfers without requiring a prior `approve` call: a user signs an authorization off-chain, and anyone (e.g., a relayer or an agentic system) can submit it to execute the transfer. This enables x402 payment protocol support for agentic transactions — any EIP-3009-compatible token can act as the payment rail for x402 flows. ## Flat fee model The spoke-chain `UTYVaultInterface` charges flat fees on cross-chain deposits and redemptions. These fees cover the LayerZero messaging cost for the spoke-to-hub and hub-to-spoke round trip. | Parameter | Token | Default | Maximum | Rationale | | ---------------- | ----- | ---------------------------------- | ---------------------------------- | -------------------------------------------------------------------------- | | `depositFlatFee` | UTY | `0.14 UTY` (`140000000000000000`) | `MAX_DEPOSIT_FEE` (`5e18` = 5 UTY) | Covers spoke-to-hub + hub-to-spoke LayerZero gas for a deposit round trip. | | `redeemFlatFee` | yUTY | `0.21 yUTY` (`210000000000000000`) | `MAX_REDEEM_FEE` (`5e18` = 5 yUTY) | Covers spoke-to-hub + hub-to-spoke LayerZero gas for a redeem round trip. | **Why flat, not percentage.** LayerZero costs are per-message (\~$0.06–$0.11 at current gas prices), not per-amount. A percentage fee would create a cross-user subsidy where large deposits overpay and dust deposits underpay. Flat fees with a small buffer achieve break-even regardless of deposit size, and the same fee funds the return message. ### Fee caps Both fees have hard-coded maximum constants: * `MAX_DEPOSIT_FEE` = `5e18` (5 UTY in OFT token units) * `MAX_REDEEM_FEE` = `5e18` (5 yUTY in OFT token units) The `setDepositFlatFee()` and `setRedeemFlatFee()` functions revert with `FeeExceedsMaximum` if the new fee exceeds the cap. This limits admin abuse even if the fee manager key is compromised — the worst-case scenario is 5 tokens per operation, not arbitrary. ## Fee collection and withdrawal Fees are collected using a `balanceOf(address(this))` sweep model: The full `assets` amount is transferred from the user into the `VaultInterface` contract. Only `assets - depositFlatFee` is bridged cross-chain via the OFT. The fee stays behind as a token balance in the `VaultInterface` contract. Over time, the `VaultInterface` contract's `balanceOf(address(this))` grows as each deposit leaves its fee behind. An address with `OPERATIONS_ROLE` (or the owner) calls `withdrawFees(token, recipient)` to sweep the entire balance to a recipient wallet. There are no accumulator variables and no drift. The sweep reads `balanceOf(address(this))` at withdrawal time. This saves \~5,000 gas per deposit versus maintaining a running total. **Safety constraint.** This sweep model is ONLY safe with mint/burn OFTs. With a lockbox OFT (`OFTAdapter`), user funds locked for bridging would be indistinguishable from fee balances. See [Spoke-only deployment safety](#spoke-only-safety) for the constructor-time check that enforces this invariant. ## Fee management roles Three operations are gated by role, and each can be performed by either the contract owner or an address holding `OPERATIONS_ROLE`: | Action | Who | | --------------- | -------------------------- | | Set deposit fee | Owner or `OPERATIONS_ROLE` | | Set redeem fee | Owner or `OPERATIONS_ROLE` | | Withdraw fees | Owner or `OPERATIONS_ROLE` | Fees are set per-chain at deploy time via the `initialize()` parameters, allowing different fee levels for chains with different gas costs. As of the current deployment, Avalanche and Katana use identical fees (0.14 UTY deposit, 0.21 yUTY redeem). # Base operations Source: https://docs.yieldpoint.io/protocol/integration/base-operations Deposit, withdraw, and claim flows that run directly on Base. UTY vault mint and redeem are Base-only; the yUTY vault is reachable either directly here or via the spoke cross-chain path. Base is the hub. All vault state lives here: shares, withdrawal requests, bonding timers, exchange rates. This page covers the flows an integrator on Base touches directly. For cross-chain flows that originate on Avalanche or Katana, see [Spoke chain operations](/protocol/integration/spoke-chain-operations). ## Upfront notes * UTY vault mint (USDC → UTY) and redeem (UTY → USDC) run only on Base. The underlying USDC custody lives here; there is no cross-chain route to USDC. To exit UTY from a spoke, bridge UTY to Base first (see [Bridge operations](/protocol/integration/bridge-operations)), then redeem. * Every deposit and redemption request requires a prior ERC-20 `approve` on the underlying token granting the vault allowance. * Bonding periods are configurable vault state, not protocol constants. Call `getBondingPeriod()` on each vault as the authoritative source; at the time of writing, yUTY returns `0` (claims available the same block as the request) and UTY returns `7 days`. Integrators building bonding-aware logic should read the function, not hard-code the value. ## Deposit USDC into the UTY vault UTY is USDC-backed at a strict 1:1 peg. You deposit USDC; the vault mints UTY 1:1; USDC is swept to the custodian wallet by the vault's extension logic. `USDC.approve(UTY_VAULT, amount)` on Base. `UTY.deposit(assets, receiver)` — `assets` is the USDC amount; `receiver` is the address that receives the minted UTY. **Common failure.** If the deposit amount underflows the vault's internal accounting, the call reverts with `FeeExceedsAmount(fee, amount)`. See [Gotchas](/protocol/integration/gotchas#feeexceedsamount-on-deposit-or-redeem-request). ## Deposit UTY into the yUTY vault yUTY is an ERC-4626-style yield-bearing vault over UTY. The exchange rate is not 1:1 — the mainnet yUTY vault has received donations, so `totalAssets() != totalSupply()`. Use `convertToAssets(shares)` and `convertToShares(assets)` to convert accurately; don't assume a 1:1 relationship. `UTY.approve(YUTY_VAULT, amount)` on Base. `yUTY.deposit(assets, receiver)` — `assets` is the UTY amount; `receiver` receives the yUTY shares. ## Withdraw from yUTY yUTY uses the ERC-7540 async request-and-claim pattern. The bonding period is currently `0`, so the claim is available in the same block as the request — but the call sequence is still two steps. `yUTY.requestRedeem(shares, controller, owner)` — burns `shares`, creates a `WithdrawalRequest`, and returns a `requestId`. `controller` is the address authorized to claim; `owner` must be `msg.sender` or an approved operator. `yUTY.redeemById(requestId, receiver)` — transfers the underlying UTY to `receiver`. Call this in the next transaction once the request confirms, or in the same transaction if your integration batches both calls. To drain multiple matured requests in a single call, see [Hub claim paths](#hub-claim-paths) for the `redeem` and `withdraw` alternatives. **Common failure.** If `msg.sender` isn't the `controller` and isn't approved via `setOperator` on the hub, the claim reverts with `InvalidRequest()` — an overloaded error that also surfaces when the request doesn't exist or was already claimed. See [Gotchas](/protocol/integration/gotchas#controller-mismatch-invalidrequest-vs-inputmustbesender) for the debugging recipe. ## Withdraw from UTY UTY redemption (UTY → USDC) runs through the same ERC-7540 pattern but the bonding period is 7 days. The spoke doesn't gate the claim client-side — if you claim before the bonding period elapses, the hub rejects (with a different revert name per claim path; see the Common-failure note below). Check `request.unlockTime` against `block.timestamp` before claiming. `UTY.approve(UTY_VAULT, amount)` on Base (required even for redemption because the vault transfers shares internally). `UTY.requestRedeem(shares, controller, owner)` — burns UTY shares, creates a `WithdrawalRequest` with `unlockTime = block.timestamp + getBondingPeriod()`. 7 days at current configuration. Read `getBondingPeriod()` for the authoritative value. `UTY.redeemById(requestId, receiver)` — transfers USDC to `receiver`. To drain multiple matured requests in a single call, see [Hub claim paths](#hub-claim-paths) for the `redeem` and `withdraw` alternatives. **Common failure.** Premature claim revert names differ per path: * `redeemById` reverts with `BondingPeriodNotOver()`. * `redeem` reverts with `ERC4626ExceededMaxRedeem(controller, shares, maxShares)` because `maxRedeem(controller)` returns `0` during bonding. * `withdraw` reverts with `ERC4626ExceededMaxWithdraw(controller, assets, maxAssets)` because `maxWithdraw(controller)` returns `0` during bonding. Check `request.unlockTime` against `block.timestamp` before claiming on any path. See [Gotchas](/protocol/integration/gotchas#premature-claim-reverts-uty-only). ## Hub claim paths The hub vault exposes three claim functions. All three require the bonding period to have elapsed and apply the same controller-or-operator authorization rules described above. They differ in how they select which request or requests to consume. | Function | Denomination | Targets | Partial-drain | When to use | | ---------------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | `redeemById(requestId, receiver)` | full request | one specific request by ID | no — closes the entire request | Per-request accounting where the caller tracks individual request IDs | | `redeem(shares, receiver, controller)` | shares | consumes the controller's matured requests in storage order until the requested share total is fulfilled | yes — partially consumes the trailing request when the share total runs out mid-request | Aggregate claim across multiple matured requests in a single call, sized in shares | | `withdraw(assets, receiver, controller)` | assets | consumes the controller's matured requests in storage order until the requested asset total is fulfilled | yes — partially consumes the trailing request when the asset total runs out mid-request | Aggregate claim across multiple matured requests in a single call, sized in assets | * `redeem` and `withdraw` skip non-matured requests in the loop, so a single call cleanly handles a queue that mixes matured and bonding requests. * All three paths are hub-only. None are exposed on the spoke `UTYVaultInterface` — see [Spoke claim constraints](/protocol/integration/spoke-chain-operations#spoke-claim-constraints). * **Partners doing per-request accounting (gauges, vault-of-vaults, anything that tracks individual request IDs) should use `redeemById`.** It is the only path with a stable caller-controlled request identity. Iteration order across multiple matured requests in `redeem` and `withdraw` is implementation-defined and must not be relied on for accounting. ## Function reference `previewRedeem()` and `previewWithdraw()` are ERC-4626-native and revert with `ERC7540NotSupported` on these async vaults. Use `previewRequestRedeem()` and `previewRequestWithdraw()` for expected-output preview. ### UTY hub vault | Function | Purpose | Who can call it | Returns | | ----------------------------------------------------- | ------------------------------------------------------------------------------------- | ----------------------------------------- | --------------------- | | `deposit(assets, receiver)` | Mint UTY at 1:1 to `receiver` | Anyone (after USDC `approve`) | `uint256 shares` | | `requestRedeem(shares, controller, owner)` | Open a UTY → USDC redemption request | `owner` (or operator) | `uint256 requestId` | | `redeemById(requestId, receiver)` | Claim a settled redemption | `controller` of the request (or operator) | `uint256 assets` | | `redeem(shares, receiver, controller)` | Claim across the controller's matured requests in storage order | `controller` (or operator) | `uint256 assets` | | `withdraw(assets, receiver, controller)` | Claim a target asset amount across the controller's matured requests in storage order | `controller` (or operator) | `uint256 shares` | | `previewRequestRedeem(shares)` | Expected USDC output for a given share burn | Anyone (view) | `uint256 assets` | | `getBondingPeriod()` | Current bonding period in seconds | Anyone (view) | `uint256` | | `getWithdrawalRequests(controller, offset, limit)` | Paginated list of a controller's pending requests | Anyone (view) | `WithdrawalRequest[]` | | `claimableRedeemRequest(requestId, controller)` | Claimable shares for a specific request | Anyone (view) | `uint256 shares` | | `convertToAssets(shares)` / `convertToShares(assets)` | Exchange-rate conversions | Anyone (view) | `uint256` | | `totalAssets()` / `totalSupply()` | Vault state | Anyone (view) | `uint256` | | `maxRedeem(controller)` | Maximum claimable shares right now | Anyone (view) | `uint256` | ### yUTY hub vault | Function | Purpose | Who can call it | Returns | | -------------------------------------------------- | ------------------------------------------------------------------------------------- | --------------------------------------------------- | --------------------- | | `deposit(assets, receiver)` | Mint yUTY shares from UTY (ERC-4626) | Anyone (after UTY `approve`) | `uint256 shares` | | `requestRedeem(shares, controller, owner)` | Open a yUTY → UTY redemption request | `owner` (or operator) | `uint256 requestId` | | `redeemById(requestId, receiver)` | Claim a settled redemption | `controller` (or operator) | `uint256 assets` | | `redeem(shares, receiver, controller)` | Claim across the controller's matured requests in storage order | `controller` (or operator) | `uint256 assets` | | `withdraw(assets, receiver, controller)` | Claim a target asset amount across the controller's matured requests in storage order | `controller` (or operator) | `uint256 shares` | | `previewRequestRedeem(shares)` | Expected UTY output for a given share burn | Anyone (view) | `uint256 assets` | | `getBondingPeriod()` | Current bonding period in seconds (currently `0`) | Anyone (view) | `uint256` | | `getWithdrawalRequests(controller, offset, limit)` | Paginated list of pending requests | Anyone (view) | `WithdrawalRequest[]` | | `setOperator(operator, approved)` | Delegate claim authority over your own requests | Anyone — delegates `msg.sender`'s own requests only | `bool` | | `isOperator(controller, operator)` | Check operator approval | Anyone (view) | `bool` | ### The `WithdrawalRequest` struct ```solidity theme={"dark"} struct WithdrawalRequest { uint256 requestId; uint256 assets; uint256 shares; uint256 requestTime; uint256 unlockTime; address controller; } ``` Note what's **not** in this struct: there's no origin-chain field. A request made from an Avalanche spoke and a request made directly on Base are indistinguishable in hub state. See [Gotchas](/protocol/integration/gotchas#multi-spoke-origin-tracking) for the implications. ## Event reference This section is the canonical home for the cross-chain event correlation recipe. The [Bridge operations](/protocol/integration/bridge-operations) and [Spoke chain operations](/protocol/integration/spoke-chain-operations) pages link here rather than re-derive it. ### Hub vault (`UTYAsyncVaultV1`, on Base) | Event | When it fires | Key fields | | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | | `Deposit(sender, owner, assets, shares)` | On `deposit()` | Standard ERC-4626 | | `Withdraw(sender, receiver, owner, assets, shares)` | On any hub claim (`redeemById`, `redeem`, or `withdraw`) — fires once per call with aggregated `assets` and `shares` | Standard ERC-4626 | | `RedeemRequest(controller, owner, requestId, sender, assets)` | On `requestRedeem()` | `requestId`, `controller` | | `RequestClosed(requestId, controller, receiver, assets)` | Once per fully-closed request — always once per `redeemById`; fires `0..N` times per `redeem` or `withdraw` call (only on full-drain branches; the trailing partial-drain request emits no `RequestClosed`) | `requestId` | | `Donation(from, assets)` | On `donate()` (yUTY only; UTY `donate()` reverts) | Increases yUTY share price | ### Hub composer (`UTYVaultComposer`, on Base) | Event | When it fires | Key fields | | ------------------------------------------------------- | --------------------------------------------------------- | ---------------------------- | | `RedeemRequested(...)` | On spoke-originated redemption requests after `lzCompose` | OFT message `guid` | | `GasTankDebited(guid, operationType, remainingBalance)` | Each outbound LayerZero message from the hub to a spoke | `guid` (the correlation key) | | `GasTankFunded(remainingBalance)` | Native token sent to the Composer | — | | `RefundPending(id, token, amount, receiver)` | A cross-chain action couldn't complete; retriable | `id` for `retryRefund(id)` | | `RefundCompleted(id)` | Refund retry succeeded | — | ### Spoke `VaultInterface` (`UTYVaultInterface`, on each spoke — included here for correlation) The spoke emits no per-request event. Partner indexers watching for the on-spoke handle on a cross-chain request use `GasTankDebited`'s `guid` field as the correlation key. | Event | When it fires | | ------------------------------------------------------- | --------------------------------------------------------- | | `FeeCollected(token, amount)` | Flat fee taken on deposit or redeem request | | `FeesWithdrawn(token, recipient, amount)` | Operations sweeps accumulated fees | | `FlatFeeUpdated(feeType, oldValue, newValue)` | Fee parameter change (timelocked) | | `GasTankFunded(remainingBalance)` | Native token sent to the spoke interface | | `GasTankDebited(guid, operationType, remainingBalance)` | Each outbound LayerZero message from the spoke to the hub | | `GasTokensRecovered(to, amount, remainingBalance)` | Operations withdraws excess native from the gas tank | ### Correlation recipe For a cross-chain flow initiated on a spoke, match the spoke's `GasTankDebited.guid` to the corresponding hub event: * **Withdrawal request (spoke → hub):** spoke `GasTankDebited.guid` → hub vault `RedeemRequest` (the `guid` appears in the hub composer's `RedeemRequested` event, which is emitted immediately before the vault's `RedeemRequest`). * **Deposit return-hop (hub → spoke):** watch the hub composer's `GasTankDebited.guid` as the signal that the deposit's return message has been paid for by the protocol's hub gas tank. * **Claim return-hop (hub → spoke):** watch the spoke `VaultInterface`'s `GasTankDebited.guid` for the claim-phase return trip. If the hub-side event never arrives after the typical LayerZero latency window (under a minute in practice, with no SLA), check LayerZero Scan at `https://layerzeroscan.com/tx/` — the message may be queued pending a gas-tank refill. See [Gotchas: `InsufficientFunds`](/protocol/integration/gotchas#insufficientfunds-from-gas-tank-depletion). ## Why `donate()` reverts on UTY but works on yUTY The UTY vault overrides `donate()` to revert. UTY maintains a strict 1:1 peg with USDC — a donation would inflate the share price and break the peg. The yUTY vault allows `donate()`; that's the mechanism by which yield is distributed to yUTY holders (the per-share price increases). # Bridge operations Source: https://docs.yieldpoint.io/protocol/integration/bridge-operations LayerZero V2 messaging, the DVN trust model, on-chain verification recipe, and the protocol-level defenses that layer on top of LayerZero. If you haven't read the [Overview](/protocol/integration/overview), the `VaultInterface`, OFT adapter, and hub/spoke mental model are defined there. Start at the Overview if any terms below read as unfamiliar. YieldPoint's cross-chain story is built on LayerZero V2. UTY and yUTY travel between Base, Avalanche, and Katana as OFTs; vault operations that originate on a spoke (deposit yUTY, request redeem, claim) reach the hub via LayerZero compose messages. This page covers the trust model, the verification recipe, the token-bridging walkthroughs, and the protocol-level defenses that sit on top of LayerZero's verification layer. ## DVN trust model Every cross-chain pathway is configured with a required threshold of **at least 2 independent DVNs** (Decentralized Verifier Networks). No single DVN can forge a message. Providers are public, audited infrastructure operators: | Pathway | Required DVNs | | ---------------- | ----------------------------- | | Base ↔ Avalanche | LayerZero Labs + Google Cloud | | Base ↔ Katana | LayerZero Labs + Nethermind | There is no single point of failure in the bridge — a compromise of any one DVN operator's key is not sufficient to pass verification. ## Verify the DVN configuration yourself DVN configuration is on-chain state, not something you should take on faith from docs. To verify the threshold and providers for a pathway, call `getConfig` on the LayerZero V2 endpoint: ```solidity theme={"dark"} // On the chain you want to verify (e.g., Base) // Returns the encoded ULN (UltraLightNode) config for the given pathway bytes memory config = ILayerZeroEndpointV2(endpoint).getConfig( oApp, // The YieldPoint OApp address on this chain (adapter, OFT, or composer) sendOrReceiveLib, // The send or receive library address for this pathway remoteEid, // The LayerZero endpoint ID of the remote chain uint32(2) // CONFIG_TYPE_ULN ); UlnConfig memory uln = abi.decode(config, (UlnConfig)); // uln.requiredDVNCount -- must be >= 2 // uln.requiredDVNs -- both entries must be populated and non-zero // uln.confirmations -- block confirmations required before the message can be verified ``` **Pass criterion.** A healthy YieldPoint pathway satisfies: * `uln.requiredDVNCount >= 2` * Every entry in `uln.requiredDVNs` is populated and non-zero * `uln.confirmations` is non-zero (block-finality requirement) If any of those fails, the pathway is misconfigured and you should not rely on it. **Endpoint addresses.** LayerZero maintains the [canonical registry of V2 endpoints](https://docs.layerzero.network/v2/deployments/deployed-contracts). Look up the endpoint for your chain there; this guide intentionally does not inline any endpoint, DVN, or library addresses because that information drifts and the registry is authoritative. ## Bridge UTY or yUTY between chains UTY and yUTY both use LayerZero's OFT standard. The hub (Base) side is a **lockbox adapter** (`ShareOFTAdapter`): tokens sent cross-chain are locked in the adapter, not burned. The spoke side is a **mint/burn OFT** (`OFTExtended`): tokens arriving from the hub mint; tokens leaving burn. ### Base → spoke 1. Call `send()` on the hub adapter for the token you're sending: `UTY ShareOFTAdapter` (on Base) for UTY, `yUTY ShareOFTAdapter` (on Base) for yUTY. 2. The adapter locks your tokens on Base and submits a LayerZero message. 3. On arrival at the spoke, the OFT mints the equivalent amount to the receiver address you specified. ### Spoke → Base 1. Call `send()` on the spoke OFT for the token you're sending: `UTY OFTExtended` (on the spoke) for UTY, `yUTY OFTExtended` (on the spoke) for yUTY. 2. The spoke OFT burns your tokens and submits a LayerZero message. 3. On arrival at Base, the hub adapter unlocks the equivalent amount from its lockbox to the receiver address. The adapter/OFT pair differs by token (UTY has its own adapter and OFTs; yUTY has its own). The [Contracts page](/protocol/architecture/contracts) lists the deployed addresses per chain. ## Gas sponsorship The protocol sponsors return-hop LayerZero fees for cross-chain vault operations. Users and integrators pay only the first-hop native fee on the originating chain; the protocol pays the return-hop fee from gas tanks maintained on each spoke `VaultInterface` and on the hub `UTYVaultComposer`. This is what lets the spoke `VaultInterface` preserve the ERC-7540 nonpayable signatures exactly — partners can call `deposit` and `requestRedeem` on a spoke without attaching `msg.value` for cross-chain fees. Under normal operation the gas tanks are funded and monitored by YieldPoint ops; partners don't manage tank balances day-to-day. The failure mode matters for integrator design: **If a gas tank is empty**, the cross-chain message reverts with `InsufficientFunds(availableFunds, requiredFunds)` at the originating contract (either the spoke `VaultInterface` for outbound messages, or the hub `UTYVaultComposer` for return-hop messages). Funds are not lost — the LayerZero message is queued at the endpoint and retries are permissionless — but the integration-facing signal is "my transaction reverted" or "the return-hop event never arrived within the expected latency window." See [Gotchas: `InsufficientFunds`](/protocol/integration/gotchas#insufficientfunds-from-gas-tank-depletion) for detection and recovery. ## Latency and observability Cross-chain messages typically settle in under a minute for a full spoke → hub → spoke round trip. **No SLA.** Latency depends on DVN liveness, destination chain congestion, and executor queue depth — none of which the protocol controls. Rather than blocking on a return message, watch on-hub state via an indexer or a Base RPC. The cross-chain event correlation recipe (spoke `GasTankDebited.guid` → hub `RedeemRequest` or hub composer `GasTankDebited`) lives in the [Base operations event reference](/protocol/integration/base-operations#event-reference) — that page is the canonical home; use the `guid` field as your correlation key. ## Protocol-level defenses beyond DVN verification LayerZero's multi-DVN verification handles message authenticity. YieldPoint layers additional protocol-level defenses on top, covered briefly here: * **Trusted remotes on the Composer.** `UTYVaultComposer` validates incoming `lzCompose` messages against a `trustedRemotes` mapping. Messages from unregistered spoke contracts are rejected outright — an attacker who compromises every DVN still can't get a message processed unless they originate it from a registered `VaultInterface`. * **Per-vault `COMPOSER_ROLE`.** The hub vault grants `COMPOSER_ROLE` to exactly one `UTYVaultComposer` per vault. There is no cross-vault authority — a bug in one composer cannot act on another vault. * **Bridge-level pause.** `ShareOFTAdapter` inherits `PausableWithPauserAuth`. Admin EOAs can pause cross-chain share-token transfers as an emergency control, independent of the vault's own pause. * **Per-controller pending-claims counter.** Each spoke `VaultInterface` tracks a per-controller counter: `requestRedeem` increments it; `redeem` requires it to be `> 0` and decrements it. Without a prior request, a `redeem` call on the spoke reverts with `NoPendingClaim` — this prevents zero-cost gas-tank drain attacks. ## Cross-chain trust model The diagram below shows a cross-chain message flow with the required-DVN verification step highlighted. A message from a spoke OApp must be signed by every DVN in the required-DVN set before the hub OApp accepts it; the threshold is at least 2 and the required-DVNs list must be populated with non-zero addresses (the pass criterion from the verification recipe above). ```mermaid theme={"dark"} flowchart LR A["Spoke OApp
(VaultInterface or OFTExtended)"] B{{"Required DVN set
(≥ 2 independent providers)"}} C["Hub OApp
(Composer or ShareOFTAdapter)"] A -->|"send()"| B B -->|"all DVNs attest"| C C -.->|"verify on-chain via
endpoint.getConfig(...)"| D["Integrator"] ``` *Figure: Cross-chain message flow with required-DVN verification. A message leaves the spoke OApp, passes through the required-DVN verification set (each DVN independently attests), and arrives at the hub OApp only after the threshold is met. Integrators verify the DVN configuration directly on-chain via `endpoint.getConfig` — the recipe above returns the `UlnConfig` struct with the current thresholds and providers.* # Gotchas and known limits Source: https://docs.yieldpoint.io/protocol/integration/gotchas Partner integration pitfalls and their fixes: controller parameter, InputMustBeSender vs InvalidRequest, multi-spoke origin tracking, and common revert signatures. Terms like `controller`, `VaultInterface`, and the ERC-7540 async request/claim pattern are defined in [Overview](/protocol/integration/overview) and [Base operations](/protocol/integration/base-operations). Start there if anything below reads as unfamiliar. This page is organized by the failure you're debugging. Each entry names the error, the likely cause, and the fix. The entries use three presentation styles: * **Prose** when the failure applies identically on both hub and spoke (e.g., exchange-rate drift, preview reverts). * **Chain label in the heading** when the failure only fires on one chain (e.g., `NoPendingClaim` is spoke-only). * **Tabs for hub vs spoke** when the same semantic failure surfaces as a different error depending on where you call it. ## Controller parameter `controller` in `requestRedeem(shares, controller, owner)` is the address authorized to claim the resulting withdrawal request. It is the most important parameter for cross-chain integrators to get right. **Two patterns:** * **Pattern 1 — contract is controller.** Works on the hub vault only. The contract can call `redeemById` on the hub vault directly to claim. **Does not work on a spoke** — spoke `redeem` enforces `msg.sender == controller` with no operator-delegation path, and a contract that lives on a spoke cannot directly call the hub vault to recover. * **Pattern 2 — user EOA is controller.** Works on both hub and spoke. The user calls `redeem` or `redeemById` directly. This is the correct pattern for any cross-chain integration where the user should be able to claim from a spoke. **If your contract needs users to claim from a spoke, use Pattern 2.** Passing a contract address as `controller` on a spoke `requestRedeem` permanently prevents that contract from claiming from the spoke, and no `setOperator` delegation can rescue a Pattern-1-on-spoke integration (see [Multi-spoke origin tracking](#multi-spoke-origin-tracking) for the details). ## Controller mismatch: `InvalidRequest` vs `InputMustBeSender` Same semantic failure ("the caller isn't authorized to claim this request"), different error surface depending on where you called `redeem` — this is a chain-variant failure. **Error:** `InvalidRequest()` — *overloaded*. The hub vault uses this single error for three distinct failures: 1. The `requestId` doesn't exist. 2. The request was already claimed (the vault zeros `unlockTime` on claim). 3. The caller is not the `controller` and not an approved operator. **Debugging recipe:** 1. Recall that `controller` is the address authorized to claim. 2. Call `getWithdrawalRequest(requestId)`. If *this* call also reverts with `InvalidRequest()`, the request doesn't exist or was already claimed. 3. If `getWithdrawalRequest` returns a `WithdrawalRequest` struct, compare `req.controller` to the caller you were using for `redeemById`. 4. If they don't match, check operator approval with `isOperator(req.controller, msg.sender)`. An approved operator can also claim (see [`setOperator` semantics](#setoperator-semantics) below). **Error:** `InputMustBeSender(address input)` — named argument, different selector, different semantics. The spoke `VaultInterface.redeem(shares, receiver, controller)` enforces `_checkInputIsSender(controller)` — `controller` must equal `msg.sender`. **There is no operator-delegation path on spokes.** The spoke `VaultInterface` is a cross-chain proxy; it cannot validate operator approvals that live on the hub without a synchronous hub-state read, which it does not perform. **Debugging recipe:** 1. The `input` field in the error is the address you passed as `controller`. 2. If it isn't equal to `msg.sender`, that's the bug. Either call from the `controller` EOA directly (Pattern 2), or bridge the hub recovery path — call `redeemById` on the hub vault on Base using the `controller` account. 3. If your integration is a contract that cannot make hub calls, you're in the Pattern-1-on-spoke trap; see the controller-safety warning above. ## `setOperator` semantics `setOperator(operator, approved)` is an ERC-7540 standard function, and it's **permissionless** — think of it as `ERC-20.approve()` but for vault operations. * `setOperator(operator, approved)` writes `isOperator[msg.sender][operator] = approved`. It only delegates access to **the caller's own** withdrawal requests. There's no admin role or front-running concern — a third party calling `setOperator` only grants access to *their own* requests, not yours. * The hub vault additionally reverts if `operator == msg.sender` (`OperationNotAllowed`). A contract cannot self-delegate via `setOperator`; this closes a self-operator trick that would otherwise let Pattern-1-on-spoke contracts rescue themselves. * Operator delegation only works on the hub vault. Spokes do not honor it (see the `InputMustBeSender` tab above). ## Multi-spoke origin tracking **Scope declaration.** YieldPoint's integration guide does not support the multi-spoke-concurrent-request pattern. If your controller has pending withdrawal requests on more than one spoke at the same time, you are outside the supported workflow. This section describes the failure modes for readers who land here from a failing integration, plus a detection recipe and — where possible — a recovery path. ### Why it breaks The hub vault processes withdrawal requests in **chain-agnostic FIFO** order. The `WithdrawalRequest` struct has no `sourceEid` or origin-chain field — a request issued from Avalanche and a request issued from Katana are indistinguishable in hub storage. Each spoke tracks its own per-controller `pendingClaims` counter, which increments on `requestRedeem` and decrements on `redeem`, but the counters don't sync cross-chain. Three failure modes arise from this mismatch: * **Cross-spoke sweeping.** A `redeem` call from spoke A can settle requests originated from spoke B, because the hub processes FIFO and doesn't know the origin. * **Partial stranding.** If a spoke claim consumes its ticket for less than the full hub-side request amount, the remaining shares are only claimable directly from the hub, not from either spoke. * **Orphaned tickets.** After cross-spoke sweeping, the "source" spoke still has `pendingClaims > 0` (its counter wasn't decremented because the hub settled into a different spoke's claim). A follow-up `redeem` on the source spoke consumes the ticket on the spoke side (counter decrements, LZ fee debited) but the hub rejects with `ERC4626ExceededMaxRedeem` (or similar) because the matching hub state has already been settled. The ticket is burned, the gas tank is debited, and nothing arrives. ### Pre-claim detection recipe Before calling `redeem` on a spoke, confirm no concurrent cross-spoke claim is in flight: 1. Call `getWithdrawalRequests(controller, offset, limit)` on the hub vault on Base — note the three-arg signature. See the [Base operations function reference](/protocol/integration/base-operations#function-reference) for the full return shape. 2. Compare the hub array length to the sum of your own per-spoke `pendingClaims(controller)` counts across all spokes your integration tracks. 3. If the hub count exceeds the sum of your per-spoke counts, a concurrent request is in flight from a chain you're not tracking. Either wait for the in-flight round trip to settle, or claim directly on the hub with `redeemById(requestId, receiver)`. **Origin chain is not recoverable from hub state.** You cannot ask the hub "which spoke did this request come from" — that data isn't stored. The count-mismatch check is the best you can do without your own per-spoke ledger. ### Recovery preconditions If you're already stranded (a spoke ticket is burned, a hub request is stuck), recovery depends on which controller pattern you chose: * **Pattern 2 (user EOA as controller):** The user calls `redeemById(requestId, receiver)` on the hub vault on Base directly. This requires the user to transact on Base — hold ETH for gas and switch chains in their wallet. It works but the UX is "leave the spoke." * **Pattern 1 (contract as controller):** If the contract lives on Base, call `redeemById` directly. If the contract lives on a spoke and cannot call Base, **there is no clean recovery path.** The contract cannot self-delegate (`setOperator` reverts on self), and the user EOA cannot claim either (they're not the controller). Partners who reach this state should escalate to YieldPoint integration support via Discord `#integrations`. * **Design-time fix:** use Pattern 2 from the start. If you're reading this because you're stuck, pattern choice is the root cause — future integrations of the same flow should adopt Pattern 2. ## `FeeExceedsAmount` on deposit or redeem request **Error:** `FeeExceedsAmount(uint256 fee, uint256 amount)`. Source contract: `UTYVaultInterface` (on each spoke) for the three-arg `deposit(assets, receiver, controller)` and `requestRedeem(shares, controller, owner)` calls. Also fires on the hub UTY vault's own deposit path for the same underflow condition. The flat deposit and redeem fees are deducted from the amount before bridging. If the amount is less than or equal to the fee, the call reverts — the vault won't accept a zero-value or negative-value post-fee amount. **Fix.** Before calling, read the current fee and require strict inequality: * For `requestRedeem(shares, ...)`: require `shares > redeemFlatFee()`. * For `deposit(assets, ...)`: require `assets > depositFlatFee()`. The `fee` and `amount` fields on the error tell you exactly how far under the threshold you were — useful for UI that wants to display "minimum redeemable" hints. ## `NoPendingClaim()` on the spoke The spoke `VaultInterface.redeem` requires `pendingClaims[controller] > 0` before it fires the cross-chain message. Without a prior `requestRedeem` on the **same spoke** from the **same controller**, the call reverts with `NoPendingClaim()`. The counter is per-controller and per-spoke. A `requestRedeem` on Avalanche does not create a claim ticket on Katana. If you request on Avalanche and try to claim on Katana, you get `NoPendingClaim()` — which is also the signal that the design is multi-spoke and you should revisit the scope declaration above. The slot is also **per-request**, not per-share. Each `requestRedeem` adds one slot regardless of share size, and each `redeem` consumes one slot regardless of how many shares are claimed. A controller with three matured requests on the same spoke therefore needs three separate spoke `redeem` calls — calling `redeem(largeShareTotal, ...)` once will consume only the first slot, succeed, and leave the other two requests unclaimed (the second call will succeed too; the fourth will revert with `NoPendingClaim()`). To drain multiple matured requests in a single call, claim on the hub via `redeem` or `withdraw` (see [Hub claim paths](/protocol/integration/base-operations#hub-claim-paths)). ## Premature-claim reverts (UTY only) Applies to the UTY vault, which has a non-zero bonding period (7 days at current config). The spoke does not gate the claim client-side — it forwards the cross-chain message, and the hub rejects when the request is not yet matured. The hub revert name depends on which claim path was called: | Claim path | Premature-claim revert | | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `redeemById(requestId, receiver)` | `BondingPeriodNotOver()` | | `redeem(shares, receiver, controller)` | `ERC4626ExceededMaxRedeem(controller, shares, maxShares)` (because `maxRedeem(controller)` returns `0` during bonding) | | `withdraw(assets, receiver, controller)` | `ERC4626ExceededMaxWithdraw(controller, assets, maxAssets)` (because `maxWithdraw(controller)` returns `0` during bonding) | **Fix:** check `request.unlockTime` against `block.timestamp` before calling any of the three. For yUTY (`bondingPeriod == 0`), this check is trivially satisfied in the next block. ## Preview functions revert on async vaults `previewRedeem()` and `previewWithdraw()` are ERC-4626-native and don't know about ERC-7540's request-and-claim pattern. On the hub vaults they revert with `ERC7540NotSupported`. Use the async equivalents instead: | Don't call | Call this instead | | ------------------------- | -------------------------------- | | `previewRedeem(shares)` | `previewRequestRedeem(shares)` | | `previewWithdraw(assets)` | `previewRequestWithdraw(assets)` | Both return the expected paired value (`assets` for shares, or `shares` for assets) at the current exchange rate. ## Exchange-rate drift on yUTY The mainnet yUTY vault has received donations (that's how yield is distributed — `donate()` increases the per-share value), so `totalAssets() != totalSupply()` and a yUTY share is worth more than one UTY at any given time. Use `convertToAssets(shares)` and `convertToShares(assets)` for accurate conversions; don't assume 1:1. UTY does not have this drift because the UTY vault overrides `donate()` to revert (preserving the 1:1 peg with USDC). ## `InsufficientFunds` from gas-tank depletion **Error:** `InsufficientFunds(uint256 availableFunds, uint256 requiredFunds)`. Source contracts: `UTYVaultInterface` (on each spoke, for outbound spoke → hub messages) and `UTYVaultComposer` (on the hub, for return-hop messages back to a spoke). Under normal operation, YieldPoint ops keeps the gas tanks funded. Depletion is rare but possible — if it happens, this is how to recognize and recover: **Observable signal.** A spoke-side `GasTankDebited` event is emitted without the corresponding hub-side event (hub `RedeemRequest` for a withdrawal request, or hub composer `GasTankDebited` for a deposit return-hop) arriving within the typical LayerZero latency window (under a minute). If you see a spoke `GasTankDebited.guid` with no matching hub event after a reasonable wait, suspect gas-tank depletion. **Funds are safe.** The LayerZero message is queued at the endpoint, not lost. LZ message retries are permissionless — once ops refills the relevant gas tank, anyone can retry the message, and the user need not take any action. No funds are at risk during the depletion window. **Inspection.** Look up the stuck message at `https://layerzeroscan.com/tx/` — LayerZero Scan is the canonical viewer for message status. **Escalation.** If the message remains unexecuted beyond the typical latency window (say, more than a few minutes), contact the YieldPoint integration team via Discord `#integrations`. # Integration overview Source: https://docs.yieldpoint.io/protocol/integration/overview How YieldPoint's hub-and-spoke architecture shapes integration, plus a map of where to go for the flow you need. YieldPoint is a hub-and-spoke protocol. Base is the hub: all vault state — shares, withdrawal requests, bonding timers, exchange rates — lives there. Avalanche and Katana are spokes. On spokes you interact with `VaultInterface`, a smart contract that implements the ERC-7540 vault signatures and routes deposit and redeem calls cross-chain to the hub on Base via LayerZero. ## Where do I go? | You want to… | Read… | | ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | | Deposit USDC for UTY, or UTY for yUTY, from Base | [Base operations](/protocol/integration/base-operations) | | Integrate a gauge, wrapper, or aggregator on a spoke chain | [Spoke chain operations](/protocol/integration/spoke-chain-operations) | | Verify the DVN configuration, understand the cross-chain trust model, or bridge tokens between chains | [Bridge operations](/protocol/integration/bridge-operations) | | Debug a revert, trace an error, or check a known limitation | [Gotchas and known limits](/protocol/integration/gotchas) | ## Contract surface per chain | Chain | Role | What lives here | | ----------------- | ----- | ------------------------------------------------------------------------------------------- | | Base | Hub | UTY vault, yUTY vault, hub `UTYVaultComposer`, `ShareOFTAdapter` lockboxes for UTY and yUTY | | Avalanche, Katana | Spoke | UTY OFT, yUTY OFT, `UTYVaultInterface` (one per spoke, distinct addresses) | Deployed addresses for every contract above are on the [Contracts](/protocol/architecture/contracts) page. Don't hard-code addresses — link to that page and read from it. ## Why the spoke interface looks standard The spoke `VaultInterface` matches the ERC-7540 function signatures exactly. `deposit(assets, receiver, controller)` and `requestRedeem(shares, controller, owner)` on a spoke have the same shape as on the hub vault, so partner integrations that target ERC-7540 compile against the spoke without cross-chain-specific code paths. The protocol sponsors return-hop LayerZero fees from gas tanks on the spoke `VaultInterface` and the hub `UTYVaultComposer` — users pay only the first-hop native fee. See [Bridge operations](/protocol/integration/bridge-operations) for the gas sponsorship model. **Planning a gauge or multi-chain aggregator?** If you intend to aggregate redemptions across multiple spoke chains for the same controller address, read [Gotchas and known limits](/protocol/integration/gotchas) before designing your controller model. Concurrent pending withdrawal requests across multiple spokes for one controller is **not supported** and produces failure modes the guide does not work around. # Spoke chain operations Source: https://docs.yieldpoint.io/protocol/integration/spoke-chain-operations Deposit yUTY and withdraw from a spoke chain (Avalanche, Katana). Spokes hold no vault state; they proxy calls cross-chain to the hub on Base via LayerZero. This page assumes you've read [Base operations](/protocol/integration/base-operations). Spoke chains hold no vault state — they proxy calls cross-chain to the hub on Base via LayerZero. The ERC-7540 request/claim pattern and the canonical function and event references are on the Base operations page. A spoke integration is a cross-chain integration by construction: every spoke-originated operation crosses LayerZero at least once, and some operations cross it twice (deposit back, claim back). This page covers the flows an integrator on Avalanche or Katana touches, the inline access-control semantics of the spoke `VaultInterface`, and the gotcha that trips up most partners (the `controller` parameter). ## Upfront notes * Cross-chain round trips typically settle in under a minute. **No SLA.** See [Bridge operations](/protocol/integration/bridge-operations#latency-and-observability) for the latency discussion. * The spoke `VaultInterface` address differs per spoke (one on Avalanche, one on Katana). They are not the same contract. Look up the current addresses on the [Contracts page](/protocol/architecture/contracts); do not hard-code them from another source. * **UTY vault mint (USDC → UTY) and redeem (UTY → USDC) are not available from a spoke.** The UTY token itself bridges freely via OFT (see [Bridge operations](/protocol/integration/bridge-operations#bridge-uty-or-yuty-between-chains)) — it's only the vault operations against USDC that are Base-only. ## Why UTY mint/redeem is Base-only The UTY vault uses the `UTYAsyncVaultV1Custodian` extension (introduced on [Base operations](/protocol/integration/base-operations)): the custodian sweeps deposited USDC to an off-chain wallet and the vault tracks `totalManagedAssets` for the off-chain portion. USDC settlement runs through the custodian on Base — there's no cross-chain equivalent. If you hold UTY on a spoke and want USDC, bridge UTY to Base first (see [Bridge operations](/protocol/integration/bridge-operations#spoke-base)), then call `UTY.requestRedeem` on the hub. ## Deposit yUTY from a spoke One user transaction initiates the flow; the protocol handles the hub-side deposit and the return hop. `UTY.approve(VAULT_INTERFACE, amount)` on the spoke. The `VaultInterface` address is specific to the spoke. `VaultInterface.deposit(assets, receiver, controller)` on the spoke. The three-arg form enforces `_checkInputIsSender(controller)` — `controller` must equal `msg.sender`. The two-arg form `deposit(assets, receiver)` is permissionless and is usually the right choice when a user is depositing for themselves. The `VaultInterface` takes a flat deposit fee in UTY (see `depositFlatFee()` for the current value) and forwards the remaining UTY cross-chain. The hub `UTYVaultComposer` receives the compose message, calls `yUTY.deposit` on the vault on the user's behalf, and mints yUTY shares. The vault mints shares into the yUTY `ShareOFTAdapter` lockbox, which emits a LayerZero message back to the originating spoke. On arrival, the spoke yUTY OFT mints the shares to the `receiver` address. Protocol pays this return-hop LZ fee from the hub Composer's gas tank. Partners watching for deposit settlement should subscribe to the hub composer's `GasTankDebited` event on Base — that `guid` is the signal that the return-hop message has been paid for. The spoke yUTY OFT also emits an OFT receive event when the shares arrive. See [Base operations event reference](/protocol/integration/base-operations#event-reference) for the full correlation recipe. ## Controller-safety warning Passing a contract address as `controller` on a spoke `requestRedeem` call will permanently prevent that contract from claiming on the spoke. Spoke `redeem()` enforces `msg.sender == controller` with no operator-delegation path, and the hub-side recovery primitive `setOperator` cannot save a contract-as-controller integrator (see [Gotchas: multi-spoke origin tracking](/protocol/integration/gotchas#multi-spoke-origin-tracking)). **If your contract needs users to claim from the spoke, pass the user's EOA as `controller` at request time.** This is the single most common partner integration bug. ## Spoke claim constraints The spoke `UTYVaultInterface` exposes only one claim function — `redeem(shares, receiver, controller)`. The richer hub claim API is **not available on spokes**: * **No `withdraw(assets, ...)`** — claims from spokes are share-denominated only. Partners that need to target a specific asset payout amount must claim on the hub, or convert the desired asset amount to shares before calling spoke `redeem`. * **No `redeemById(...)`** — spokes cannot target a specific request. The hub composer always invokes the aggregating `redeem(shares, ...)` loop on the vault, so requests are consumed in storage order. Partners doing per-request accounting (gauges, vault-of-vaults) must keep that accounting on the hub and use [`redeemById`](/protocol/integration/base-operations#hub-claim-paths) directly on Base. Operator delegation is also unavailable on spokes — see the [Controller-safety warning](#controller-safety-warning) above for the recovery story. UTY has no spoke claim path at all. UTY claims are Base-only regardless of which chain the holder is on; bridge UTY to Base first, then call `redeemById` (or `redeem` / `withdraw`) on the hub vault. ## Withdraw yUTY from a spoke yUTY spoke withdrawal is **two user transactions** — the protocol doesn't round-trip a settlement message, so there's no way to combine request and claim into a single tx. Between the two, the user (or their integration) watches hub state for settlement. `yUTY.approve(VAULT_INTERFACE, shares)` on the spoke. `VaultInterface.requestRedeem(shares, controller, owner)` on the spoke. Enforces `_checkInputIsSender(owner)` — `owner` must equal `msg.sender`. `controller` is arbitrary (that's how Pattern 2 works: pass the user's EOA so the user can claim from the spoke directly). A flat redeem fee is deducted from `shares` before bridging; see the fee guard below. No return message is sent. Partners watch on-hub state via indexer or Base RPC; the correlation recipe lives in the [Base operations event reference](/protocol/integration/base-operations#event-reference) — use the spoke `GasTankDebited.guid` to match the hub `RedeemRequest`. `VaultInterface.redeem(shares, receiver, controller)` on the spoke. Enforces `_checkInputIsSender(controller)` — `controller` must equal `msg.sender`; **no operator delegation on spokes**. The claim sends a second cross-chain message; the hub composer calls the vault's aggregating `redeem(shares, address(this), controller)` using its `COMPOSER_ROLE`; UTY bridges back to the spoke to the `receiver` address. For yUTY the bonding period is `0`, so this tx can run as soon as the hub confirms the request — typically within a minute of tx 1. Each spoke `redeem` call consumes exactly one matured-request slot regardless of share size, so a controller with N matured requests on this spoke needs N spoke `redeem` calls. See [Gotchas: `NoPendingClaim()`](/protocol/integration/gotchas#nopendingclaim-on-the-spoke) for the failure mode if a caller assumes one large `redeem` will drain multiple requests. **Multi-spoke claim coordination.** If the same controller has pending withdrawal requests on more than one spoke, the hub processes them in FIFO order regardless of origin chain — a `redeem` call from one spoke can consume a request that originated from another. This guide does not support the multi-spoke-concurrent-request pattern. See [Gotchas: multi-spoke origin tracking](/protocol/integration/gotchas#multi-spoke-origin-tracking). ## Fee-underflow guard The flat deposit fee and flat redeem fee are deducted from the amount before bridging. If the amount is less than or equal to the fee, the call reverts with `FeeExceedsAmount(fee, amount)` — named `fee` and `amount` fields, easy to match on. Read `redeemFlatFee()` or `depositFlatFee()` to get the current fee and require strict inequality: `shares > redeemFlatFee()` for requestRedeem, `assets > depositFlatFee()` for deposit. ## Preview reads are hub-only Spokes hold no vault state, so every read function that inspects vault accounting (`previewRequestRedeem`, `convertToAssets`, `convertToShares`, `totalAssets`, `getBondingPeriod`, `getWithdrawalRequests(controller, offset, limit)` — note the three-arg signature) is only callable on the hub vault on Base. Integrators previewing a redemption or reading withdrawal-request state must RPC against Base. The canonical function signatures and return shapes are on the [Base operations function reference](/protocol/integration/base-operations#function-reference). ## Spoke event reference The spoke `VaultInterface` emits no per-request event. To correlate a spoke request to its hub-side settlement, use the cross-chain event correlation recipe in the [Base operations event reference](/protocol/integration/base-operations#event-reference) — the spoke's `GasTankDebited` event carries the OFT message `guid` that ties the spoke-side transaction to the hub's `RedeemRequest` (for withdrawal) or the hub composer's `GasTankDebited` (for deposit return-hops). Events emitted on the spoke: | Event | When it fires | | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | `FeeCollected(token, amount)` | Flat fee taken on a deposit or redeem request | | `FeesWithdrawn(token, recipient, amount)` | Operations sweeps accumulated fees | | `FlatFeeUpdated(feeType, oldValue, newValue)` | Fee parameter change | | `GasTankFunded(remainingBalance)` | Native token sent to the spoke interface | | `GasTankDebited(guid, operationType, remainingBalance)` | Each outbound LayerZero message from the spoke — the **correlation key** for cross-chain tracing | | `GasTokensRecovered(to, amount, remainingBalance)` | Operations withdraws excess native from the gas tank | ## Cross-chain withdrawal flow The diagram below traces a yUTY withdrawal initiated on a spoke. The gap in the middle marks the asynchronous boundary: after tx 1 returns, the protocol does not send a settlement message back — partners watch hub state (via indexer or Base RPC) and initiate tx 2 when they see the hub's `RedeemRequest` event. For yUTY the bonding period is 0, so tx 2 can run as soon as the hub-side request confirms. ```mermaid theme={"dark"} sequenceDiagram participant User as User (on spoke) participant VI as Spoke VaultInterface participant Comp as Hub UTYVaultComposer (Base) participant Vault as Hub yUTY vault (Base) Note over User,VI: tx 1: request User->>VI: requestRedeem(shares, controller, owner) VI-->>VI: emit FeeCollected, GasTankDebited(guid) VI->>Comp: LayerZero compose message Comp->>Vault: requestRedeem(...) Vault-->>Vault: burn shares, emit RedeemRequest Note over User,Vault: No return message — partner watches hub state
via indexer or Base RPC, matching spoke GasTankDebited.guid
to hub RedeemRequest Note over User,VI: tx 2: claim (bondingPeriod == 0 for yUTY) User->>VI: redeem(shares, receiver, controller) VI-->>VI: emit GasTankDebited(guid) VI->>Comp: LayerZero compose message Comp->>Vault: redeemById(...) with COMPOSER_ROLE Vault-->>Comp: transfer UTY Comp->>VI: return-hop OFT message (UTY to receiver on spoke) ``` *Figure: yUTY withdrawal from a spoke chain. Two user transactions bracket an indexer-watched gap. The protocol pays the return-hop gas (tx 2) from the hub Composer's gas tank on Base.* # Security Source: https://docs.yieldpoint.io/protocol/security YieldPoint security model, audit summary, and trust assumptions. This page is being migrated from the YieldPoint contracts wiki. It covers the security model, audit history, role-based access control, timelock operations, and trust assumptions. In the meantime: * **Security questions:** [Discord](https://discord.gg/fdxe4efA2M) * **Announcements:** [@YieldPointCap on X](https://x.com/YieldPointCap) # MCP Server Source: https://docs.yieldpoint.io/protocol/tooling/mcp-server Model Context Protocol server exposing YieldPoint state and actions to AI agents. A Model Context Protocol server for YieldPoint is in development. It will expose protocol state and transaction-building tools so AI agents can read vault data and prepare deposit, redeem, withdraw, claim, and bridge calls through a standard MCP interface. Until it ships, the [API](https://api.yieldpoint.io/docs/json) exposes read-only protocol state, and the [Integration](/protocol/integration/overview) section documents the contract surface for direct on-chain interaction. * **Community:** [Discord](https://discord.gg/fdxe4efA2M) * **Announcements:** [@YieldPointCap on X](https://x.com/YieldPointCap) # Payable Router Source: https://docs.yieldpoint.io/protocol/tooling/payable-router Router contract for accepting payments in arbitrary tokens and settling in UTY or yUTY. A payable router is in development. It will let merchants and applications accept payments in arbitrary tokens and settle in UTY or yUTY, with routing handled inside a single contract call. Until it ships, the [Integration](/protocol/integration/overview) section covers direct deposit and redeem flows against the underlying vaults. * **Community:** [Discord](https://discord.gg/fdxe4efA2M) * **Announcements:** [@YieldPointCap on X](https://x.com/YieldPointCap) # SDK Source: https://docs.yieldpoint.io/protocol/tooling/sdk Typed client for reading YieldPoint state and submitting deposit, redeem, withdraw, claim, and bridge transactions. A typed client for YieldPoint is in development. It will wrap the contract surface across Base, Avalanche, and Katana so integrators can read vault state and submit deposit, redeem, withdraw, claim, and bridge transactions without hand-rolling LayerZero fee quoting and chain routing. Until it ships, the [Integration](/protocol/integration/overview) section documents the contract surface directly, and the [API](https://api.yieldpoint.io/docs/json) exposes read-only protocol state. * **Community:** [Discord](https://discord.gg/fdxe4efA2M) * **Announcements:** [@YieldPointCap on X](https://x.com/YieldPointCap) # x402 & Agents Source: https://docs.yieldpoint.io/protocol/tooling/x402 UTY and yUTY support EIP-3009 and EIP-2612 today across Base, Avalanche, and Katana. Hosted x402 endpoints are on the way. # Agentic ready by design UTY and yUTY are built to settle agent-initiated payments. The signature standards that the [x402 protocol](https://x402.org) and similar agentic flows depend on are implemented on the token contracts today, on every chain UTY and yUTY are deployed to. ## What you can use today * **EIP-3009** — `transferWithAuthorization` and `receiveWithAuthorization`. An agent signs an authorization off-chain; a resource server or relayer submits it on-chain. This is the signature scheme x402 settles with. * **EIP-2612** — `permit`. Off-chain approval signatures for adjacent flows that route through a contract rather than a direct transfer. Both are available on UTY, yUTY, and the spoke OFTs on Base, Avalanche, and Katana. **Signer caveat.** Authorization signatures are verified with ECDSA, so they must come from an EOA. Smart-account wallets that rely on ERC-1271 contract signatures (Coinbase Smart Wallet, Safe, ERC-4337 accounts) won't validate against the current token contracts. Smart-account signature support is on the roadmap. ## What's coming * Hosted x402 endpoints operated by YieldPoint, so services can price and accept UTY and yUTY over HTTP without running their own facilitator. * A listing on agentic.market so agents can discover UTY and yUTY as settlement assets. Until the hosted endpoints land, the [Integration](/protocol/integration/overview) section documents the contract surface you need to wire EIP-3009 settlement directly.