> For the complete documentation index, see [llms.txt](https://docs.buoy.finance/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.buoy.finance/developers/smart-contract-integration.md).

# Smart Contract Integration

Everything a user does on Buoy goes through the **Router**. This page covers the calls and events you need to integrate deposits, withdrawals, and vault discovery. Addresses are on [Contract Addresses](/developers/contract-addresses.md).

## Units & conventions

* **USDC amounts:** 6 decimals (EVM side). `1 USDC = 1_000_000`.
* **Shares (Buoys):** 6 decimals, matching USDC.
* **Share price:** 18-decimal fixed point (`1e18` = 1.00 USDC per share).
* **Fees:** basis points (`bps`, 1/100 of a percent); builder fees in **decibps** (1/10 of a bp).
* **Request fee:** fixed native HYPE amount, sent as `msg.value` with each request (read it from the Router — `nativeDepositFee()` / `nativeWithdrawFee()`; currently 0.01 HYPE each).

## Router — user-facing functions

```solidity
// Deposit USDC into a vault. Requires prior USDC approval to the Router.
// msg.value must equal nativeDepositFee() exactly.
function requestDeposit(
    address vault,
    address recipient,     // who receives the shares
    uint256 amount,        // USDC, 6 decimals, >= vault.minDepositUsdc()
    uint256 minShares      // slippage floor; settlement below this reverts
) external payable returns (uint256 globalId);

// Redeem shares for USDC. No approval needed — shares are escrowed directly.
// msg.value must equal nativeWithdrawFee() exactly.
function requestWithdrawal(
    address vault,
    uint256 shares,
    uint256 minOut         // USDC floor, 6 decimals
) external payable returns (uint256 globalId);

// Reclaim funds from an unsettled request (only after Router.requestTimeout()).
function cancelDeposit(uint256 globalId) external;
function cancelWithdrawal(uint256 globalId) external;

// Deploy a new vault. Pulls VaultFactory.VAULT_CREATION_SEED_USDC
// (1_000_001 = 1.000001 USDC) from msg.sender — requires approval.
struct CreateVaultParams {
    string name;
    string ticker3;        // exactly 3 characters; symbol becomes "buoy" + ticker3
    address leader;
    uint16 performanceFeeBps;   // <= 5000 (50%)
    // Deposit rules (vault v3) — 0 / false = off. See "Deposit rules" below.
    uint16 minLeaderShareBps;   // <= 5000; fixed forever once set
    uint256 maxTvlUsdc;         // net-NAV ceiling, USDC 6 dec
    bool whitelistEnabled;      // launch whitelist-only
}
function createVault(CreateVaultParams calldata params) external returns (address vault);
```

`createVault` gained the three deposit-rule fields with vault v3 (September 2026); the old 4-field struct no longer exists on the Router. When the whitelist is on, `requestDeposit` on that vault additionally requires `recipient == msg.sender` for anyone but the Leader.

The `globalId` returned by a request is the handle for tracking and cancellation. Request state can be read back with `getGlobalRequest(globalId)`, which returns `(vault, localId, kind)`.

{% hint style="info" %}
`msg.value` must **equal** the current fee, not merely cover it (`"ROUTER: bad fee"`). Read the fee in the same block you build the transaction, or take it from the REST API's quote response (`nativeFeeWei`).
{% endhint %}

Cancellation rules:

* Available only after the request timeout has passed without settlement. The vault owner (protocol admin) may also cancel a timed-out request on the requester's behalf — funds always go back to the original requester.
* A cancelled **deposit** refunds the escrowed USDC in full.
* A cancelled **pending withdrawal** returns the escrowed shares unchanged. A cancelled **locked withdrawal** re-mints shares worth the *locked USDC value* at the current share price, capped at the originally escrowed share count — once locked, the position is a fixed USDC claim, so a NAV rise after the lock does not convert back into extra shares.

## Events to index

All emitted per request, carrying the `globalId`:

| Event                 | Meaning                                                                                                                                                                                                                                                                              |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `DepositRequested`    | User submitted a deposit; USDC escrowed                                                                                                                                                                                                                                              |
| `DepositFulfilled`    | Shares minted at settlement NAV                                                                                                                                                                                                                                                      |
| `DepositCancelled`    | User reclaimed USDC after timeout                                                                                                                                                                                                                                                    |
| `DepositSkipped`      | An item in a batch settlement was skipped, with the reason — the request stays open                                                                                                                                                                                                  |
| `WithdrawalRequested` | User submitted a withdrawal; shares escrowed                                                                                                                                                                                                                                         |
| `WithdrawalLocked`    | Payout fixed in USDC; shares burned                                                                                                                                                                                                                                                  |
| `WithdrawalFulfilled` | USDC paid to the user                                                                                                                                                                                                                                                                |
| `WithdrawalSkipped`   | An item in a batch settlement was skipped, with the reason                                                                                                                                                                                                                           |
| `WithdrawalCancelled` | User reclaimed shares (Pending) or had them re-minted at the locked USDC value (Locked) after timeout                                                                                                                                                                                |
| `NavPublished`        | A NAV was cached on the vault outside any request. Fired when a Leader uses **Crystallize fees** (the fulfiller publishes a fresh NAV on their behalf, rate-limited per vault); there is no periodic heartbeat, so don't build on it firing on a schedule                            |
| `VaultCreated`        | New vault deployed. Emitted by the **VaultFactory**. Two signatures exist: vaults created before v3 emitted `(vault, leader, name, symbol, performanceFeeBps)`; since v3 the event also carries `minLeaderShareBps, maxTvlUsdc, whitelistEnabled` — a different topic, so index both |

{% hint style="warning" %}
Only `DepositRequested` and `WithdrawalRequested` are indexed by **`vault`**. Every settlement event (`WithdrawalLocked`, `*Fulfilled`, `*Cancelled`, `*Skipped`) is indexed by **`globalId` only** — there is no vault topic to filter on. Tracking one vault therefore takes two stages: collect its `globalId`s from the `*Requested` events, then watch settlement events for those ids.
{% endhint %}

Settlement is **batched** where possible: `fulfillDeposits` / `fulfillWithdrawals` settle many requests of one vault against a single NAV snapshot. Individual items that cannot settle are emitted as `DepositSkipped` / `WithdrawalSkipped` rather than reverting the batch — so a missing `*Fulfilled` event is not an error, just a request that stays open for the next attempt.

Two events are emitted by the **vault** (not the Router) whenever a performance fee crystallizes — on a deposit settlement, a withdrawal lock, or a NAV publication at a new high:

| Vault event                                            | Meaning                                                                                                                        |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
| `LeaderFeeCrystallized(currentNav, feeShares, newHwm)` | The Leader's portion of the fee, minted as shares; `feeShares` is the Leader's part only                                       |
| `ProtocolFeeCrystallized(feeShares, recipient)`        | The **referral fee** portion, minted to the RewardsDistributor (vault v2+; absent when the vault's `protocolFeeShareBps` is 0) |

{% hint style="info" %}
The contracts use `protocolFee*` naming throughout for what the product and the rest of these docs call the **referral fee** — the slice of each performance fee that funds the [Referral Program](/referral-program/referral-program.md). Same number, two names: use the contract names when calling the ABI.
{% endhint %}

## Reading vault state

Each vault exposes ERC-20 plus valuation and configuration views:

```solidity
function pricePerShare() external view returns (uint256);   // 1e18 scale, net NAV
function totalAssets() external view returns (uint256);     // net NAV, USDC 6 dec
function convertToShares(uint256 usdc) external view returns (uint256);
function convertToAssets(uint256 shares) external view returns (uint256);

function asset() external view returns (address);
function leader() external view returns (address);
function performanceFeeBps() external view returns (uint16);
function protocolFeeShareBps() external view returns (uint16);  // the referral fee, as a slice of the fee (v2+)
function protocolFeeRecipient() external view returns (address);// the RewardsDistributor (v2+)
function highWaterMark() external view returns (uint256);   // 1e18 scale
function minDepositUsdc() external view returns (uint256);
function currentTradingAgent() external view returns (address);

function latestNav() external view returns (uint256);       // last cached GROSS nav
function latestNavTimestamp() external view returns (uint64);
function totalOwedUsdc() external view returns (uint256);   // locked withdrawal obligations
function pendingDepositsUsdc() external view returns (uint256);
function interactionNonce() external view returns (uint256);// stale-snapshot guard
function nextLocalRequestId() external view returns (uint256);

// One atomic read of everything above — what the settlement service prices against.
function getNavSnapshot() external view returns (
    uint256 nonce, uint256 pendingDeposits, uint256 owed,
    uint256 evmUsdcBalance, uint256 supply,
    uint256 cachedNav, uint64 cachedNavTs
);

// Per-request state. localId space is SHARED between deposits and withdrawals,
// so ids run 1..nextLocalRequestId() and the wrong getter returns status None.
function getDepositRequest(uint256 localId) external view returns (DepositRequest memory);
function getWithdrawalRequest(uint256 localId) external view returns (WithdrawalRequest memory);
```

Request statuses are `None | Pending | Fulfilled | Cancelled` for deposits and `None | Pending | Locked | Released | Cancelled` for withdrawals — `Released` is what the app calls *Fulfilled*.

{% hint style="info" %}
Vaults are **request-based**, not ERC-4626: the familiar `convertTo*` views exist for quoting, but there are no synchronous `deposit`/`redeem` functions.

Critically, the on-chain valuation views (`pricePerShare`, `totalAssets`, `convertTo*`) read the vault's **cached** NAV, and that cache is only written when a request settles. No periodic NAV heartbeat runs in production, so on a vault with no recent deposits or withdrawals `latestNav` can be **hours or days old** while the vault's real value moves with every tick of the market. Check `latestNavTimestamp()` before trusting any of them, and for a live figure use the [REST API](/developers/rest-api.md) — its quote endpoints and `tvl` are computed from live Hyperliquid account state, not from the cache. If you are automating against a vault, read [Automating a Vault Strategy](/developers/automating-a-strategy.md) first. A published NAV of zero is valid (it reports a total strategy loss), so `totalAssets()` and `pricePerShare()` can legitimately read `0` — handle that without dividing by them.
{% endhint %}

## Deposit rules (VaultPolicy, vault v3+)

Vaults created since v3 hold a pointer to a shared **VaultPolicy** contract (address on [Contract Addresses](/developers/contract-addresses.md)) that enforces the Leader's deposit rules: a minimum leader share (skin in the game), a TVL cap and a depositor whitelist with per-address position caps. Product-level description: [Deposit Limits](/for-vault-leaders/deposit-limits.md).

```solidity
// BuoyVault (v3 only — pre-v3 vaults have no such function; the call reverts)
function policy() external view returns (address);

// VaultPolicy — reads, all keyed by vault
function configOf(address vault) external view
    returns (uint16 minLeaderShareBps, bool whitelistEnabled, uint256 maxTvlUsdc);
function depositCapOf(address vault, address account) external view returns (uint256);   // position cap, USDC 6 dec; 0 = not whitelisted
function depositedUsdcOf(address vault, address account) external view returns (uint256);// net capital in the position (used against the cap)

// The one call an integrator needs before quoting a deposit: the largest
// amount `recipient` could request right now under all three rules.
// type(uint256).max when nothing binds. Advisory (cached NAV, counts
// pending deposits as minted); ignores minDepositUsdc and pause state.
function maxDepositFor(address vault, address recipient) external view returns (uint256);

// VaultPolicy — writes, vault leader or protocol admin only
function setMaxTvlUsdc(address vault, uint256 newCap) external;                 // 0 = no cap
function setWhitelistEnabled(address vault, bool enabled) external;
function setDepositCaps(address vault, address[] calldata accounts, uint256[] calldata caps) external; // cap 0 removes
```

The minimum leader share has **no setter** by design — it is fixed by `createVault`.

Events, emitted by the **VaultPolicy** contract and indexed by `vault`:

| Event                                                                     | Meaning                                                           |
| ------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `VaultConfigured(vault, minLeaderShareBps, maxTvlUsdc, whitelistEnabled)` | Creation-time rules recorded (same transaction as `VaultCreated`) |
| `MaxTvlUpdated(vault, oldCap, newCap)`                                    | TVL cap changed                                                   |
| `WhitelistEnabledUpdated(vault, enabled)`                                 | Whitelist switched on or off                                      |
| `DepositCapUpdated(vault, account, cap)`                                  | An address's position cap set (`cap == 0` = removed)              |

Custom errors (surface as the revert reason of `requestDeposit`, or as the reason in a `DepositSkipped` event):

| Error                | Rule                                                                     |
| -------------------- | ------------------------------------------------------------------------ |
| `LeaderShareTooLow`  | Minting these shares would leave the Leader below the minimum share      |
| `TvlCapExceeded`     | Net NAV after the deposit would exceed the cap                           |
| `DepositCapExceeded` | Whitelist on and the recipient's position cap (or 0) would be exceeded   |
| `PayerNotRecipient`  | Whitelist on and a non-Leader is depositing to another address           |
| `LeaderShareTooHigh` | `createVault` with `minLeaderShareBps > MAX_MIN_LEADER_SHARE_BPS` (5000) |

How the checks run: the whitelist is exact at request time. The leader share and the TVL cap are **projected at request time** on the vault's cached NAV (so an over-limit request reverts immediately — no fee, nothing queued) and **re-checked at settlement** on the live NAV; a request that no longer fits is skipped with the error as its reason and stays `Pending` until vault state changes or the requester cancels after the timeout. Withdrawals never consult the policy, and the policy's own bookkeeping hooks on the exit path cannot block anything.

## Vault discovery

```solidity
// VaultFactory
function vaultsCount() external view returns (uint256);
function vaultAt(uint256 i) external view returns (address);
function allVaults(uint256 offset, uint256 limit) external view returns (address[] memory);
function isVault(address candidate) external view returns (bool);  // registry check

// Router
function legacyFactories(uint256 i) external view returns (address);
function legacyFactoriesCount() external view returns (uint256);
```

Always verify a vault address before interacting. The Router recognizes a vault if the **current factory or any retained legacy factory** registered it, so a complete client-side check enumerates `legacyFactories` too — a vault created under a previous factory is still fully serviceable until the admin retires that entry.

## Leader / operator functions

These are gated to the vault's leader (or the protocol admin) and are only relevant if you are building leader tooling:

```solidity
// Router — capital movement inside the vault's own HyperCore account
function transferBetweenSpotAndPerp(address vault, uint256 usdcAmount, bool toPerp) external;
function sendAssetToHip3(address vault, uint32 dexIndex, uint256 usdcAmount) external;
function withdrawAssetFromHip3(address vault, uint32 dexIndex, uint256 usdcAmount) external;

// BuoyVault
function rotateTradingAgent(address newAgent) external;                   // onlyLeader
function approveBuilderFee(uint64 maxFeeRate, address builder) external;  // leader or owner
```

`approveBuilderFee` is limited to whitelisted builders and capped at the vault's `maxBuilderFeeRate()` (100 decibps = 0.1% by default). Approving at rate `0` revokes.

{% hint style="info" %}
Vaults created today run in HyperCore **unified-account** mode: one spot USDC balance collateralizes the default perp DEX and every HIP-3 DEX automatically. In that mode the spot↔perp / HIP-3 transfer calls above move nothing and are unnecessary — they exist for vaults in classic split-wallet mode.
{% endhint %}

## RewardsDistributor — referral rewards

The referral fee lands on the **RewardsDistributor** and is paid out as referral rewards in USDC (see [How Referrals Work](/referral-program/referral-program.md)). The only user-facing call:

```solidity
// Pay msg.sender everything the protocol backend has attested up to
// cumulativeAmount (their LIFETIME total) and not yet paid. All-or-nothing:
// reverts if the contract's USDC can't cover the delta.
function claim(
    uint256 cumulativeAmount,   // USDC, 6 decimals — from the REST API's claim-signature endpoint
    uint256 deadline,           // unix seconds; signature expiry
    bytes calldata signature    // EIP-712 by the protocol's rewardsSigner
) external returns (uint256 amount);

function claimed(address account) external view returns (uint256); // lifetime paid to account
function rewardsSigner() external view returns (address);
event RewardsClaimed(address indexed account, uint256 amount, uint256 cumulativeAmount);
```

The signature is obtained from `POST /users/{address}/referral/claim-signature` ([REST API](/developers/rest-api.md)); it is bound to the claiming wallet and to the cumulative amount, so it cannot be replayed for profit or used by anyone else. Custom errors: `SignatureExpired`, `InvalidSignature`, `NothingToClaim` (the signed total is not above what was already paid). Operator functions (`requestRedeem`, `cancelRedeem`) are the fulfiller's; admin functions (`withdrawProtocolShare`, `rescueToken`, signer/operator setters, pause) are the protocol owner's.

Two related Router details: `Router.feeExempt(address)` marks protocol-owned callers (the distributor) whose requests pay no native fee — they must send `msg.value == 0` — and vault `ProtocolFeeCrystallized` events (above) are how a vault's protocol slice can be traced to the distributor.

## Integration checklist

1. Approve USDC to the **Router** (not the vault). The official app approves `type(uint256).max` once so repeat deposits are a single transaction.
2. Fetch a quote (share price, min shares/out, native fee) — the REST API's quote endpoints return all of it in one call, including the current request fee and the recommended slippage floor.
3. Check the amount against `vault.minDepositUsdc()` before sending — sub-minimum deposits revert with `"VAULT: deposit too small"`. On a v3 vault also check it against `VaultPolicy.maxDepositFor(vault, recipient)` (the REST deposit quote returns it as `maxDepositUsdc` when you pass `user`), and send with `recipient == msg.sender` if the vault's whitelist is on.
4. Send the request with the native fee as **exactly** `msg.value`.
5. Track the `globalId` through the events above.
6. Offer cancellation UI once `Router.requestTimeout()` (currently 30 minutes) has passed without settlement.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.buoy.finance/developers/smart-contract-integration.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
