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), then redeem.
- Every deposit and redemption request requires a prior ERC-20
approveon 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 returns0(claims available the same block as the request) and UTY returns7 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.Common failure. If the deposit amount underflows the vault’s internal accounting, the call reverts with
FeeExceedsAmount(fee, amount). See Gotchas.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, sototalAssets() != totalSupply(). Use convertToAssets(shares) and convertToShares(assets) to convert accurately; don’t assume a 1:1 relationship.
Withdraw from yUTY
yUTY uses the ERC-7540 async request-and-claim pattern. The bonding period is currently0, so the claim is available in the same block as the request — but the call sequence is still two steps.
Request
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.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 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 callredeemById early, the hub rejects with ERC4626ExceededMaxRedeem. Check request.unlockTime against block.timestamp before claiming.
Approve UTY
UTY.approve(UTY_VAULT, amount) on Base (required even for redemption because the vault transfers shares internally).Request
UTY.requestRedeem(shares, controller, owner) — burns UTY shares, creates a WithdrawalRequest with unlockTime = block.timestamp + getBondingPeriod().Wait for bonding
7 days at current configuration. Read
getBondingPeriod() for the authoritative value.Common failure. Calling
redeemById before unlockTime reverts with ERC4626ExceededMaxRedeem (the hub’s maxRedeem(controller) returns 0 during bonding). See Gotchas.Function reference
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 |
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 |
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
Event reference
This section is the canonical home for the cross-chain event correlation recipe. The Bridge operations and 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 redeemById() settlement | Standard ERC-4626 |
RedeemRequest(controller, owner, requestId, sender, assets) | On requestRedeem() | requestId, controller |
RequestClosed(requestId, controller, receiver, assets) | On redeemById() | Pairs with Withdraw |
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’sGasTankDebited.guid to the corresponding hub event:
- Withdrawal request (spoke → hub): spoke
GasTankDebited.guid→ hub vaultRedeemRequest(theguidappears in the hub composer’sRedeemRequestedevent, which is emitted immediately before the vault’sRedeemRequest). - Deposit return-hop (hub → spoke): watch the hub composer’s
GasTankDebited.guidas 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’sGasTankDebited.guidfor the claim-phase return trip.
https://layerzeroscan.com/tx/<txhash> — the message may be queued pending a gas-tank refill. See Gotchas: InsufficientFunds.
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).