> For the complete documentation index, see [llms.txt](https://docs.lagoon.finance/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.lagoon.finance/developer-hub/integration/synchronous-deposit-flow.md).

# Synchronous flow

Deposit and redeem in a single transaction when the curator enables it, with the conditions, balance changes and gotchas of each.

When the curator enables it, a vault can skip the epoch cycle of the [async flow](/developer-hub/integration/async-deposit-flow.md): `syncDeposit` mints shares and `syncRedeem` pays out assets in the same transaction, priced at the current `totalAssets / totalSupply`. This page describes **v0.6.0**; where **v0.5.x** behaves differently, a tab shows both.

## When sync is available

{% tabs %}
{% tab title="v0.6.0" %}
A sync call needs all three conditions. Read them live: the curator can change the mode and the valuation expires over time.

| Condition             | Read with                                                                               | Sync deposit            | Sync redeem            |
| --------------------- | --------------------------------------------------------------------------------------- | ----------------------- | ---------------------- |
| Sync mode allows it   | [`syncMode()`](/developer-hub/smart-contract-reference.md#syncmode)                     | `Both` or `SyncDeposit` | `Both` or `SyncRedeem` |
| Valuation still valid | [`isTotalAssetsValid()`](/developer-hub/smart-contract-reference.md#istotalassetsvalid) | `true`                  | `true`                 |
| Vault open            | —                                                                                       | `Open`                  | `Open`                 |

{% hint style="info" %}
Async requests stay available alongside sync. A request made while the valuation is valid waits for the next settlement, which can only start once the valuation has expired: at the end of its lifespan, or earlier if the curator calls [`expireTotalAssets`](/developer-hub/smart-contract-reference.md#expiretotalassets). A new valuation cannot be proposed while the current one is valid.
{% endhint %}

{% hint style="info" %}
A vault where [`isAsyncOnly()`](/developer-hub/smart-contract-reference.md#isasynconly) is `true` has permanently given up sync: its valuation is always treated as expired.
{% endhint %}
{% endtab %}

{% tab title="v0.5.x" %}
Only deposits can be synchronous: there is no sync mode and no sync redeem. A sync deposit needs the vault to be `Open` and the valuation to be valid: while [`isTotalAssetsValid()`](/developer-hub/smart-contract-reference.md#istotalassetsvalid) is `true` deposits must be synchronous, and once it expires they must be asynchronous.
{% endtab %}
{% endtabs %}

## Sync deposit

| Contract         | Method                                                                                                                      | Balance change                                                                                                                                                        |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Underlying asset | `approve(vault, amount)`                                                                                                    | none                                                                                                                                                                  |
| Vault            | [`syncDeposit(uint256 assets, address receiver, address referral)`](/developer-hub/smart-contract-reference.md#syncdeposit) | Asset: user → curator address (`safe()`). Shares minted to `receiver` (Transfer from `address(0)`)                                                                    |
| Vault            | Same, with native value (`msg.value != 0`)                                                                                  | Native balance leaves the user with no ERC-20 Transfer. The pendingSilo wraps it and forwards it: Transfer pendingSilo → curator address. Shares minted to `receiver` |

{% tabs %}
{% tab title="v0.6.0" %}
**Reverts when**

| Condition                                                                                                                                         | Error                     |
| ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
| Sync mode does not allow sync deposits                                                                                                            | `SyncOperationNotAllowed` |
| Valuation expired                                                                                                                                 | `TotalAssetsExpired`      |
| Vault not `Open`                                                                                                                                  | `NotOpen(state)`          |
| `msg.sender` or `receiver` fails [`isAllowed`](/developer-hub/smart-contract-reference.md#isallowed) (not whitelisted, blacklisted or sanctioned) | `AddressNotAllowed`       |
| `totalAssets()` + pending deposits (underlying held by the pendingSilo) + `assets` > `maxCap()`                                                   | `MaxCapReached`           |
| Native value sent but the underlying is not the wrapped native token                                                                              | `CantDepositNativeToken`  |
| Vault paused                                                                                                                                      | `EnforcedPause`           |

**Notes**

* Pass `address(0)` as `referral` when there is none. The call always emits a `Referral` event.
* An entry fee (`feeRates().entryRate`, in bps) is taken in shares. [`previewSyncDeposit(assets)`](/developer-hub/smart-contract-reference.md#previewsyncdeposit) returns the net shares.
* There is no slippage parameter. The price is the current valuation, and the valuation lifespan is the protection against a stale price.
* With native value, `assets` is ignored and replaced by `msg.value`, and no approval is needed.
  {% endtab %}

{% tab title="v0.5.x" %}
**Reverts when**

| Condition                                                                                   | Error                     |
| ------------------------------------------------------------------------------------------- | ------------------------- |
| Valuation expired: use [`requestDeposit`](/developer-hub/integration/async-deposit-flow.md) | `OnlyAsyncDepositAllowed` |
| Vault not `Open`                                                                            | `NotOpen(state)`          |
| Whitelist enabled and an involved address not whitelisted                                   | `NotWhitelisted`          |
| Native value sent but the underlying is not the wrapped native token                        | `CantDepositNativeToken`  |
| Vault paused                                                                                | `EnforcedPause`           |

**Notes**

* Pass `address(0)` as `referral` when there is none. The call always emits a `Referral` event.
* There is no entry fee and no deposit cap.
* There is no slippage parameter. The price is the current valuation, and the valuation lifespan is the protection against a stale price.
* With native value, `assets` is ignored and replaced by `msg.value`, and no approval is needed.
  {% endtab %}
  {% endtabs %}

## Sync redeem

{% hint style="info" %}
`syncRedeem` is new in v0.6.0. On v0.5.x, redemptions go through the async request flow, except the synchronous exit from a [closed vault](/developer-hub/integration/async-deposit-flow.md#exit-a-closed-vault).
{% endhint %}

| Contract | Method                                                                                                                         | Balance change                                                                             |
| -------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |
| Vault    | [`syncRedeem(uint256 shares, address receiver, uint256 minimumAssets)`](/developer-hub/smart-contract-reference.md#syncredeem) | Shares of the caller burnt (Transfer to `address(0)`). Asset: curator address → `receiver` |

**Reverts when**

| Condition                                                                                                                                         | Error                                       |
| ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
| Sync mode does not allow sync redeems                                                                                                             | `SyncOperationNotAllowed`                   |
| Valuation expired                                                                                                                                 | `TotalAssetsExpired`                        |
| Vault not `Open`                                                                                                                                  | `NotOpen(state)`                            |
| `msg.sender` or `receiver` fails [`isAllowed`](/developer-hub/smart-contract-reference.md#isallowed) (not whitelisted, blacklisted or sanctioned) | `AddressNotAllowed`                         |
| `receiver` is `address(0)`                                                                                                                        | `InvalidReceiver`                           |
| Net assets below `minimumAssets`                                                                                                                  | `BelowMinimumAssets(assets, minimumAssets)` |
| Curator address balance or allowance to the vault too low                                                                                         | Underlying token's transfer error           |
| Vault paused                                                                                                                                      | `EnforcedPause`                             |

**Notes**

* The shares are always the caller's own: there is no `owner` or `controller` argument.
* Two fees apply: the exit fee, then a haircut (`haircutRate`, 0 to 20%) on the remaining shares. When the haircut is above 0, a sync redeem costs more than an async one. [`previewSyncRedeem(shares)`](/developer-hub/smart-contract-reference.md#previewsyncredeem) returns the net assets.
* Use `minimumAssets` as a slippage guard, e.g. the `previewSyncRedeem` result: it protects against a haircut or fee change between the preview and the execution.
* A `HaircutTaken` event is emitted when a haircut applies.

{% hint style="warning" %}
Liquidity is not guaranteed. The assets come from the curator address (`safe()`), so it must hold enough underlying and have granted an allowance to the vault. Cap the input at `min(asset.allowance(safe, vault), asset.balanceOf(safe))`, converted to shares.
{% endhint %}

## Choosing the deposit path with viem

The vault state has no on-chain getter, so these snippets assume an `Open` vault. Read `state` from the [Lagoon API](#choosing-the-deposit-path-with-the-lagoon-api) and only deposit when it is `Open`.

{% tabs %}
{% tab title="v0.6.0" %}

```typescript
const SYNC_DEPOSIT_MODES = [0, 1]; // SyncMode.Both, SyncMode.SyncDeposit

const [mode, isValid] = await Promise.all([
  client.readContract({ abi: vaultAbi, address: vault, functionName: "syncMode" }),
  client.readContract({ abi: vaultAbi, address: vault, functionName: "isTotalAssetsValid" }),
]);

if (isValid && SYNC_DEPOSIT_MODES.includes(mode)) {
  await client.writeContract({
    abi: vaultAbi,
    address: vault,
    functionName: "syncDeposit",
    args: [assets, user, zeroAddress],
  });
} else {
  await client.writeContract({
    abi: vaultAbi,
    address: vault,
    functionName: "requestDeposit",
    args: [assets, user, user],
  });
}
```

{% endtab %}

{% tab title="v0.5.x" %}

```typescript
const isValid = await client.readContract({
  abi: vaultAbi,
  address: vault,
  functionName: "isTotalAssetsValid",
});

await client.writeContract({
  abi: vaultAbi,
  address: vault,
  functionName: isValid ? "syncDeposit" : "requestDeposit",
  args: isValid ? [assets, user, zeroAddress] : [assets, user, user],
});
```

{% endtab %}
{% endtabs %}

## Choosing the deposit path with the Lagoon API

The [Lagoon API](/developer-hub/lagoon-api.md) returns the same inputs in one query. On v0.5.x vaults it reports `syncMode` as `Both` and `totalAssetsExpiration` as `0` even while the valuation is valid, so for them read [`isTotalAssetsValid()`](/developer-hub/smart-contract-reference.md#istotalassetsvalid) on-chain, as in the v0.5.x tab above. Send it to `https://api.lagoon.finance/query`:

```graphql
query GetSyncDepositState($address: Address!, $chainId: Int!) {
  vaultByAddress(address: $address, chainId: $chainId) {
    state {
      version
      syncMode
      totalAssetsExpiration
      state
    }
  }
}
```

[**Try in Apollo Sandbox →**](https://api.lagoon.finance/query?document=query%20GetSyncDepositState%28%24address%3A%20Address%21%2C%20%24chainId%3A%20Int%21%29%20%7B%0A%20%20vaultByAddress%28address%3A%20%24address%2C%20chainId%3A%20%24chainId%29%20%7B%0A%20%20%20%20state%20%7B%0A%20%20%20%20%20%20version%0A%20%20%20%20%20%20syncMode%0A%20%20%20%20%20%20totalAssetsExpiration%0A%20%20%20%20%20%20state%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D\&variables=%7B%22address%22%3A%220x1dc8164e06dbc540438e38074bafc87c022c3e33%22%2C%22chainId%22%3A1%7D)

```bash
curl -sS -X POST https://api.lagoon.finance/query \
  -H "Content-Type: application/json" \
  -d '{
    "query": "query GetSyncDepositState($address: Address!, $chainId: Int!) { vaultByAddress(address: $address, chainId: $chainId) { state { version syncMode totalAssetsExpiration state } } }",
    "variables": { "address": "0xVAULT", "chainId": 1 }
  }'
```

| Field                   | Meaning                                                                 | On-chain equivalent                                        |
| ----------------------- | ----------------------------------------------------------------------- | ---------------------------------------------------------- |
| `version`               | Implementation version, e.g. `v0.6.0`                                   | `version()`                                                |
| `syncMode`              | `Both`, `SyncDeposit`, `SyncRedeem` or `None`                           | `syncMode()`                                               |
| `totalAssetsExpiration` | Unix timestamp after which the valuation is stale. `0` on v0.5.x vaults | `isTotalAssetsValid()` is `true` while it is in the future |
| `state`                 | `Open`, `Closing` or `Closed`                                           | —                                                          |

```typescript
const SYNC_DEPOSIT_MODES = ["Both", "SyncDeposit"];

const res = await fetch("https://api.lagoon.finance/query", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ query: GET_SYNC_DEPOSIT_STATE, variables: { address: vault, chainId } }),
});
const { state } = (await res.json()).data.vaultByAddress;

if (state.state !== "Open") throw new Error("Vault is not open: a deposit request would not be settled");

const [major, minor] = state.version.slice(1).split(".").map(Number);
const isValid =
  major > 0 || minor >= 6
    ? Number(state.totalAssetsExpiration ?? 0) > Date.now() / 1000
    : await client.readContract({ abi: vaultAbi, address: vault, functionName: "isTotalAssetsValid" });
const canSyncDeposit = SYNC_DEPOSIT_MODES.includes(state.syncMode) && isValid;

await client.writeContract({
  abi: vaultAbi,
  address: vault,
  functionName: canSyncDeposit ? "syncDeposit" : "requestDeposit",
  args: canSyncDeposit ? [assets, user, zeroAddress] : [assets, user, user],
});
```

{% hint style="info" %}
The valuation expires with time, so compare `totalAssetsExpiration` to the current time when sending the transaction, not when the query was made. A `Closed` vault still accepts `requestDeposit`, but the request can never be settled: the assets stay in the pendingSilo until the user cancels.
{% endhint %}

## Checking a sync redeem with the Lagoon API

`syncRedeem` only exists from v0.6.0, and the API reports `syncMode` as `Both` for v0.5.x vaults, so check `version` as well. The query also returns the curator address's balance of the underlying. Its allowance to the vault and the net output are not in the API: read them on-chain.

```graphql
query GetSyncRedeemState($address: Address!, $chainId: Int!) {
  vaultByAddress(address: $address, chainId: $chainId) {
    asset {
      address
    }
    state {
      version
      syncMode
      totalAssetsExpiration
      state
      safeAssetBalance
      roles {
        safe
      }
    }
  }
}
```

[**Try in Apollo Sandbox →**](https://api.lagoon.finance/query?document=query%20GetSyncRedeemState%28%24address%3A%20Address%21%2C%20%24chainId%3A%20Int%21%29%20%7B%0A%20%20vaultByAddress%28address%3A%20%24address%2C%20chainId%3A%20%24chainId%29%20%7B%0A%20%20%20%20asset%20%7B%0A%20%20%20%20%20%20address%0A%20%20%20%20%7D%0A%20%20%20%20state%20%7B%0A%20%20%20%20%20%20version%0A%20%20%20%20%20%20syncMode%0A%20%20%20%20%20%20totalAssetsExpiration%0A%20%20%20%20%20%20state%0A%20%20%20%20%20%20safeAssetBalance%0A%20%20%20%20%20%20roles%20%7B%0A%20%20%20%20%20%20%20%20safe%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D\&variables=%7B%22address%22%3A%220xd0c4c9386f7509c44987f43136be7d4349ccddc9%22%2C%22chainId%22%3A1%7D)

```bash
curl -sS -X POST https://api.lagoon.finance/query \
  -H "Content-Type: application/json" \
  -d '{
    "query": "query GetSyncRedeemState($address: Address!, $chainId: Int!) { vaultByAddress(address: $address, chainId: $chainId) { asset { address } state { version syncMode totalAssetsExpiration state safeAssetBalance roles { safe } } } }",
    "variables": { "address": "0xVAULT", "chainId": 1 }
  }'
```

| Field                   | Meaning                                                | On-chain equivalent                                        |
| ----------------------- | ------------------------------------------------------ | ---------------------------------------------------------- |
| `version`               | Implementation version, e.g. `v0.6.0`                  | `version()`                                                |
| `syncMode`              | `Both`, `SyncDeposit`, `SyncRedeem` or `None`          | `syncMode()`                                               |
| `totalAssetsExpiration` | Unix timestamp after which the valuation is stale      | `isTotalAssetsValid()` is `true` while it is in the future |
| `state`                 | `Open`, `Closing` or `Closed`                          | —                                                          |
| `safeAssetBalance`      | Underlying held by the curator address, in asset units | `asset.balanceOf(safe)`                                    |
| `roles.safe`            | Curator address                                        | `safe()`                                                   |

```typescript
const SYNC_REDEEM_MODES = ["Both", "SyncRedeem"];

const res = await fetch("https://api.lagoon.finance/query", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ query: GET_SYNC_REDEEM_STATE, variables: { address: vault, chainId } }),
});
const { asset, state } = (await res.json()).data.vaultByAddress;

const [major, minor] = state.version.slice(1).split(".").map(Number);
const canSyncRedeem =
  (major > 0 || minor >= 6) &&
  SYNC_REDEEM_MODES.includes(state.syncMode) &&
  Number(state.totalAssetsExpiration ?? 0) > Date.now() / 1000 &&
  state.state === "Open";

if (canSyncRedeem) {
  const [allowance, assetsOut] = await Promise.all([
    client.readContract({ abi: erc20Abi, address: asset.address, functionName: "allowance", args: [state.roles.safe, vault] }),
    client.readContract({ abi: vaultAbi, address: vault, functionName: "previewSyncRedeem", args: [shares] }),
  ]);
  const balance = BigInt(state.safeAssetBalance); // large amounts are returned as strings
  const liquidity = allowance < balance ? allowance : balance;

  if (assetsOut > 0n && assetsOut <= liquidity) { // previewSyncRedeem returns 0 when a sync redeem is not possible
    await client.writeContract({
      abi: vaultAbi,
      address: vault,
      functionName: "syncRedeem",
      args: [shares, user, assetsOut],
    });
  }
}
```

When `canSyncRedeem` is `false` or the curator address cannot cover `assetsOut`, fall back to the [async redemption](/developer-hub/integration/async-deposit-flow.md) with `claimSharesAndRequestRedeem(shares)` while the vault is `Open`. Redemption requests revert with `NotOpen` once the vault is `Closing`; once it is `Closed`, exit with `redeem` as in [Exit a closed vault](/developer-hub/integration/async-deposit-flow.md#exit-a-closed-vault).
