<pre>
title: Smart Vaults
description: Custom deposit and withdrawal logic for vaults written in WebAssembly (WASM)
created: 2026-04-11
author: Thomas Hussenet (@LeJamon), Willem Olding (@willemolding) based on Smart Escrows by Mayukha Vadari (@mvadari), David Fuelling (@sappenin)
proposal-from: Smart Escrows XLS (https://github.com/XRPLF/XRPL-Standards/discussions/270)
status: Draft
category: Amendment
</pre>
## 1. Abstract
This proposal introduces programmable deposit and withdrawal logic to XRPL Single Asset Vaults, enabling conditions beyond the current first-come-first-serve withdrawal strategy. A piece of WebAssembly (WASM) code resides on the `Vault` object itself, controlling whether deposits and withdrawals are permitted and how assets are managed. By adding programmatic conditions, vaults become versatile building blocks for DeFi, treasury management, and programmable custody on the XRPL.
## 2. Motivation
Smart Vaults extend the XRPL's on-ledger programmability to asset management, enabling use cases that require custom gating logic for deposits and withdrawals. While Smart Escrows address conditional release of held payments, Smart Vaults address programmable pooling and management of assets.
### 2.1. Example Use-Cases
- **Programmable Treasuries**: DAO treasuries where withdrawals require on-chain governance conditions.
- **Conditional Yield Vaults**: Vaults that only accept deposits meeting specific criteria (e.g., minimum amounts, credential verification).
- **Time-Locked Savings**: Vaults that reject withdrawals until a certain ledger time or sequence.
- **KYC-Gated Pools**: Only credentialed accounts may deposit or withdraw.
- **Oracle-Gated Vaults**: Deposits or withdrawals conditioned on price oracle data (e.g., only withdraw when asset price exceeds a threshold).
- **Allowlist/denylist Vaults**: Restrict vault access to specific sets of accounts.
- **Rate-Limited Vaults**: Limit deposit or withdrawal amounts per time period using the vault's `Data` field as state.
- **Auction Vaults**: Accept bids as deposits, release to winning bidder based on custom logic.
- **Insurance Pools**: Conditional payouts based on verifiable on-chain events.
- **ZK-Verified Vaults**: Deposits or withdrawals gated by zero-knowledge proof verification (using BN254 host functions).
- **Vesting Schedules**: Programmatic release of assets based on time or milestones, tracked in the `Data` field.
## 3. Overview
The design attaches a WASM code block to a Vault object that executes logic to determine whether deposits and withdrawals are permitted. This follows the same model as Smart Escrows: a restricted, on-ledger "extension" where conditions are coded and verified directly on-chain.
The WASM code is attached to the Vault on creation (in a `VaultCreate` transaction) and executed on `VaultDeposit` and `VaultWithdraw` transactions. The code must export two functions: `on_deposit` and `on_withdraw`.
Smart Vaults with the WASM withdrawal policy still use MPToken shares for on-chain ownership tracking, but the **exchange rate between assets and shares is determined by the WASM code** rather than a fixed pro-rata formula. On each deposit, the WASM code specifies how many shares to mint via the `setExchangeAmount` host function. On each withdrawal, it specifies how many asset units to release. This enables custom pricing models (bonding curves, fee structures, fixed-amount receipt tokens) while maintaining trustless ownership records that support emergency withdrawal.
This design reuses the existing WASM engine and host function infrastructure introduced by the Smart Escrow amendment.
This proposal involves:
- Modifications to the `Vault` ledger object
- Modifications to the `VaultCreate`, `VaultDeposit`, `VaultWithdraw`, `VaultSet`, and `VaultDelete` transactions
- A new `WithdrawalPolicy` value: `vaultStrategyWASM`
This feature will require an amendment, tentatively titled `SmartVault`, and depends on the `SingleAssetVault` and `WasmVM` amendments.
### 3.1. Background: XRPL Single Asset Vaults
A Single Asset Vault holds a single asset type (XRP, IOU, or MPT) and issues shares to depositors as MPTokens. The vault has a pseudo-account that holds the pooled assets. Currently, vaults support one withdrawal strategy:
- **First-Come-First-Serve** (`WithdrawalPolicy = 1`): Depositors exchange assets for shares, and withdrawers redeem shares for assets, on a pro-rata basis.
### 3.2. Background: Smart Escrows
Smart Escrows (proposed in a separate XLS) introduce WASM-based programmability to XRPL Escrows. They add a `FinishFunction` field containing compiled WASM code that controls whether an escrow can be released. Smart Vaults build on the same WASM engine, host functions, gas metering, and fee infrastructure.
### 3.3. Background: XRPL Extensions
An **XRPL Extension** is a small piece of code attached to an XRPL building block, which allows users to add custom logic to existing primitives. See the Smart Escrows spec for more details.
## 4. Ledger Entry: `Vault`
The `Vault` object already exists on the XRPL (via the `SingleAssetVault` amendment). We propose a modification to support Smart Vaults.
### 4.1. Fields
<details>
<summary>
As a reference, here are the existing fields for the `Vault` object.
</summary>
| Field Name | Required? | JSON Type | Internal Type | Description |
| :------------------ | :-------- | :-------- | :------------ | :-------------------------------------------------------------------------------------------- |
| `Owner` | Yes | `string` | `AccountID` | The account address of the Vault Owner. |
| `Account` | Yes | `string` | `AccountID` | The address of the Vault's pseudo-account. |
| `Asset` | Yes | `object` | `Issue` | The asset (XRP, IOU, or MPT) of the Vault. |
| `AssetsTotal` | Yes | `string` | `Number` | The total value of the vault. |
| `AssetsAvailable` | Yes | `string` | `Number` | The asset amount that is available in the vault. |
| `AssetsMaximum` | | `string` | `Number` | The maximum asset amount that can be held. Zero or absent means no cap. |
| `LossUnrealized` | | `string` | `Number` | The potential loss amount not yet realized. |
| `ShareMPTID` | Yes | `string` | `UInt192` | The identifier of the share MPTokenIssuance object. |
| `WithdrawalPolicy` | Yes | `number` | `UInt8` | Indicates the withdrawal strategy used by the Vault. |
| `Scale` | | `number` | `UInt8` | The scaling factor for vault shares (IOU assets only, 0-18). |
| `Data` | | `string` | `Blob` | Arbitrary metadata about the Vault, limited to 4KB for WASM vaults. |
| `Flags` | Yes | `number` | `UInt32` | Bit-map of boolean flags. |
| `Sequence` | Yes | `number` | `UInt32` | The transaction sequence number that created the vault. |
| `OwnerNode` | Yes | `string` | `UInt64` | Identifies the page where this item is referenced in the owner's directory. |
| `PreviousTxnID` | Yes | `string` | `Hash256` | The identifying hash of the transaction that most recently modified this entry. |
| `PreviousTxnLgrSeq` | Yes | `number` | `UInt32` | The index of the ledger that contains the transaction that most recently modified this entry. |
</details>
We propose one additional field:
| Field Name | Required? | JSON Type | Internal Type | Description |
| :---------- | :-------- | :-------- | :------------ | :----------------------------------------------------------------------------------------------------------- |
| `VaultCode` | | `string` | `Blob` | Compiled WebAssembly (WASM) code that controls deposit and withdrawal logic (size limits via `FeeSettings`). |
#### 4.1.1. `VaultCode`
The compiled WASM code included in this field must contain the following exported functions:
**Required:**
1. **`on_deposit`**: Takes no parameters and returns a signed integer (`int32`). If the function returns a value greater than 0, the deposit is permitted. If it returns 0 or a negative value, the deposit is rejected with `tecWASM_REJECTED`. The WASM code **must** call the `setExchangeAmount` host function exactly once before returning to specify how many shares to mint for the depositor.
2. **`on_withdraw`**: Takes no parameters and returns a signed integer (`int32`). If the function returns a value greater than 0, the withdrawal is permitted. If it returns 0 or a negative value, the withdrawal is rejected with `tecWASM_REJECTED`. The WASM code **must** call the `setExchangeAmount` host function exactly once before returning to specify how many asset units to release to the withdrawer.
**Optional:**
3. **`on_set`**: Takes no parameters and returns a signed integer (`int32`). If exported and the function returns a value greater than 0, the `VaultSet` operation is permitted. If it returns 0 or a negative value, the operation is rejected with `tecWASM_REJECTED`. If not exported, `VaultSet` proceeds with its default behavior (owner-only, standard field validation).
4. **`on_delete`**: Takes no parameters and returns a signed integer (`int32`). If exported and the function returns a value greater than 0, the `VaultDelete` operation is permitted (subject to standard deletion checks). If it returns 0 or a negative value, the operation is rejected with `tecWASM_REJECTED`. If not exported, `VaultDelete` proceeds with its default behavior.
All functions have read access to the current transaction, the vault ledger object, and other ledger objects via the host function API. They can also modify the vault's `Data` field via the `updateData` host function (except `on_delete`, where `updateData` is a no-op since the vault is being deleted if the function approves).
##### `setExchangeAmount` Host Function
A new host function available only during Smart Vault WASM execution of `on_deposit` and `on_withdraw`:
```
setExchangeAmount(amount: uint64) -> int32
```
- During `on_deposit`: specifies the number of **shares to mint** for the depositor.
- During `on_withdraw`: specifies the number of **asset units to release** to the withdrawer.
**Constraints:**
- `setExchangeAmount` must be called **exactly once** per execution. A second call returns an error code and the transaction fails with `tecWASM_REJECTED`.
- If `setExchangeAmount` is not called before the WASM function returns, the transaction fails with `tecWASM_REJECTED` (even if the return value is positive).
- `setExchangeAmount` should be the final host function call before returning, as it signals the WASM code's exchange rate decision.
- The amount is validated by the ledger after WASM execution (see Section 12.1 for invariants).
- `setExchangeAmount` is not available during `on_set` or `on_delete` execution.
##### Post-WASM Ledger Validation
After the WASM function returns successfully and `setExchangeAmount` has been called, the ledger validates the exchange amount:
- **On deposit**: the shares amount must be greater than 0. If the WASM code sets 0 shares, the transaction fails with `tecWASM_REJECTED`.
- **On withdrawal**: the asset amount must be greater than 0 and must not exceed `AssetsAvailable`. If the WASM code sets 0 assets or an amount greater than `AssetsAvailable`, the transaction fails with `tecWASM_REJECTED`.
These checks are enforced by the ledger, not the WASM code, ensuring that buggy or malicious WASM cannot silently steal deposits or overdraft the vault.
##### Why WASM Controls the Exchange Rate
Standard vaults compute shares via a fixed pro-rata formula: `shares = deposit * totalShares / totalAssets`. This works for simple pools but cannot express:
- **Bonding curves**: share price increases with supply
- **Fee structures**: entry/exit fees deducted from shares or assets
- **Privacy pools**: fixed 1:1 receipt tokens regardless of pool size
- **Staking rewards**: exchange rate improves over time
- **Custom AMM logic**: non-linear pricing functions
By letting the WASM code call `setExchangeAmount`, the vault supports all of these while still using MPToken shares as the on-chain ownership record.
##### Deposit Semantics: All-or-Nothing
Deposits are all-or-nothing. The full `Amount` specified in the `VaultDeposit` transaction is transferred to the vault, or the transaction is rejected entirely. The WASM code cannot partially accept a deposit. If the WASM code determines that the deposit should be rejected (e.g., amount too small, wrong credentials), it must return 0 or a negative value.
##### The `on_set` Function
The `on_set` function enables use cases such as:
- Validating that the `Data` field conforms to a specific schema before accepting the update.
- Implementing access control beyond owner-only (e.g., allowing a designated manager account to update vault parameters).
- Maintaining internal state invariants across `Data` updates (e.g., ensuring a counter only increases).
##### The `on_delete` Function
The `on_delete` function enables use cases such as:
- Preventing vault deletion while an auction or other time-bound process is still active (based on state in the `Data` field).
- Requiring specific conditions to be met before the vault can be torn down (e.g., all obligations settled).
Note: `on_delete` is an additional gate on owner-initiated deletion. Standard deletion checks (e.g., zero outstanding shares) still apply after the WASM function approves.
#### 4.1.2. `WithdrawalPolicy` Values
| Value | Name | Description |
| :---- | :--------------------------------- | :-------------------------------------------------------------------------- |
| `1` | `vaultStrategyFirstComeFirstServe` | Standard shares-based deposit/withdrawal with fixed pro-rata exchange. |
| `2` | `vaultStrategyWASM` | WASM-controlled deposit/withdrawal with programmable exchange rate. |
When `WithdrawalPolicy` is `vaultStrategyWASM`:
- The WASM code decides whether each deposit/withdrawal is permitted **and** the exchange rate.
- Shares (MPTokens) are always minted on deposit and burned on withdrawal — the WASM code determines the amount via `setExchangeAmount`.
- The `Data` field (4KB) stores vault-level configuration and state (merkle roots, fee parameters, counters), not per-user balances.
- Per-user ownership is tracked on-chain via MPToken share balances, enabling trustless emergency withdrawal.
### 4.2. Reserves
A `Vault` object with a `VaultCode` field costs 1 additional object reserve per 500 bytes of WASM code (beyond the first 500 bytes, which are included in the base vault reserve). The additional increment count is computed as `floor(codeSize / 500)`. This follows the same model as Smart Escrows.
For example:
- A vault with no `VaultCode`: 1 reserve increment (vault) + 1 (pseudo-account) = 2 total
- A vault with 400 bytes of `VaultCode`: 1 + 1 = 2 total (code fits in base increment)
- A vault with 1200 bytes of `VaultCode`: 1 + 1 + 2 = 4 total (floor(1200/500) = 2 extra increments + base + pseudo-account)
## 5. Transaction: `VaultCreate`
The `VaultCreate` transaction already exists on the XRPL. We propose a modification to support Smart Vaults.
### 5.1. Fields
We propose one additional field:
| Field Name | Required? | JSON Type | Internal Type | Description |
| :---------- | :-------- | :-------- | :------------ | :--------------------------------------------------------------------------- |
| `VaultCode` | | `string` | `Blob` | Compiled WASM code that controls deposit and withdrawal logic for the vault. |
Rules:
- If `VaultCode` is included, `WithdrawalPolicy` must be set to `2` (`vaultStrategyWASM`).
- If `WithdrawalPolicy` is `2`, `VaultCode` must be included.
- The WASM code must export both `on_deposit` and `on_withdraw` functions with the correct signatures. The `on_set` and `on_delete` exports are optional.
### 5.2. Transaction Fee
A `VaultCreate` with a `VaultCode` costs the owner reserve (as with any vault creation) plus an additional upload fee:
$$fee = ownerReserve + (baseFee \times 9) + (5 \times codeSize)$$
Where `codeSize` is the number of bytes in the `VaultCode` field.
### 5.3. Failure Conditions
The existing failure conditions still apply. These failure conditions are added when `VaultCode` is included:
- `VaultCode` is empty (zero bytes).
- `VaultCode` exceeds the `ExtensionSizeLimit` specified in `FeeSettings`.
- The WASM code is not valid, or does not export the required `on_deposit` and `on_withdraw` functions.
- `WithdrawalPolicy` is not `2` when `VaultCode` is present.
- `WithdrawalPolicy` is `2` but `VaultCode` is not present.
- The `SmartVault` amendment is not enabled.
### 5.4. State Changes
The `VaultCode` field is stored on the `Vault` ledger object. The owner count is increased by the variable reserve amount based on code size (see Section 4.2).
## 6. Transaction: `VaultDeposit`
The `VaultDeposit` transaction already exists on the XRPL. We propose a modification to support Smart Vaults.
### 6.1. Fields
We propose one additional field:
| Field Name | Required? | JSON Type | Internal Type | Description |
| :--------------------- | :-------- | :-------- | :------------ | :-------------------------------------------------------------------------------------------------------------------------------- |
| `ComputationAllowance` | | `number` | `UInt32` | The amount of gas the user is willing to pay for the execution of the WASM code. Required if the `Vault` has a `VaultCode` field. |
### 6.2. Transaction Fee
There is a higher transaction fee for executing a `VaultDeposit` when the vault has a `VaultCode`:
$$fee = baseFee + \frac{ComputationAllowance \times GasPrice}{1{,}000{,}000} + 1$$
Where `GasPrice` is from the `FeeSettings` ledger object, denominated in micro-drops.
### 6.3. WASM Execution
When a `VaultDeposit` is submitted for a vault with `WithdrawalPolicy = 2`:
1. The `VaultCode` is loaded from the vault ledger object.
2. A host function context is created with access to the transaction and vault state.
3. The `on_deposit` function is executed with the specified `ComputationAllowance` as the gas limit.
4. During execution, the WASM code must call `setExchangeAmount(sharesAmount)` exactly once to specify how many shares to mint.
5. If the function returns a value greater than 0, the ledger validates the exchange amount:
- `sharesAmount` must be > 0.
- If validation passes, the deposit proceeds:
- The full `Amount` from the transaction is transferred from the depositor to the vault pseudo-account.
- The amount specified by `setExchangeAmount` is used as the number of shares to mint.
- Shares (MPTokens) are minted and transferred to the depositor.
- Vault `AssetsTotal` and `AssetsAvailable` are updated.
6. If the function returns 0 or a negative value, the transaction fails with `tecWASM_REJECTED`.
7. If `setExchangeAmount` was not called, or was called more than once, the transaction fails with `tecWASM_REJECTED`.
8. If the ledger validation of the exchange amount fails, the transaction fails with `tecWASM_REJECTED`.
9. If the WASM execution modifies the `Data` field (via `updateData`), the change is persisted.
10. `GasUsed` and `WasmReturnCode` are recorded in the transaction metadata.
### 6.4. Failure Conditions
The existing failure conditions still apply. These failure conditions are added:
- `ComputationAllowance` is included, but the vault doesn't have a `VaultCode` (`tefNO_WASM`).
- The vault has a `VaultCode`, but `ComputationAllowance` isn't included (`tefWASM_FIELD_NOT_INCLUDED`).
- The vault is in emergency mode (`tecEMERGENCY_MODE`).
- The `ComputationAllowance` provided is not enough gas to complete the WASM execution.
- The `on_deposit` function returns 0 or a negative number (`tecWASM_REJECTED`).
- `setExchangeAmount` was not called, was called more than once, or returned a shares amount of 0 (`tecWASM_REJECTED`).
### 6.5. Metadata Changes
| Field Name | Validated? | Always Present? | Type | Description |
| :--------------- | :--------- | :-------------- | :------- | :------------------------------------------------------------------ |
| `GasUsed` | Yes | Conditional | `UInt32` | The amount of gas actually used by the computation of `on_deposit`. |
| `WasmReturnCode` | Yes | Conditional | `Int32` | The integer code returned by the `on_deposit` function. |
## 7. Transaction: `VaultWithdraw`
The `VaultWithdraw` transaction already exists on the XRPL. We propose a modification to support Smart Vaults.
### 7.1. Fields
We propose one additional field:
| Field Name | Required? | JSON Type | Internal Type | Description |
| :--------------------- | :-------- | :-------- | :------------ | :-------------------------------------------------------------------------------------------------------------------------------- |
| `ComputationAllowance` | | `number` | `UInt32` | The amount of gas the user is willing to pay for the execution of the WASM code. Required if the `Vault` has a `VaultCode` field. |
### 7.2. Transaction Fee
Same formula as `VaultDeposit` (see Section 6.2).
### 7.3. WASM Execution
Same model as `VaultDeposit`, but the `on_withdraw` function is executed instead:
1. The WASM code must call `setExchangeAmount(assetAmount)` exactly once to specify how many asset units to release.
2. The ledger validates the exchange amount: `assetAmount` must be > 0 and must not exceed `AssetsAvailable`.
3. On success:
- The shares specified in the transaction's `Amount` field are burned from the withdrawer.
- The asset amount specified by `setExchangeAmount` is transferred from the vault pseudo-account to the destination (or submitter if no `Destination` specified).
- Vault `AssetsTotal` and `AssetsAvailable` are updated.
Note: The withdrawer submits a `VaultWithdraw` with their share amount. The WASM code decides how many assets those shares are worth, which may differ from the standard pro-rata calculation.
### 7.4. Emergency Withdrawal
If the vault is in emergency mode (see Section 13.4), `VaultWithdraw` bypasses WASM execution entirely. The `ComputationAllowance` field must not be provided. The withdrawal uses standard pro-rata redemption:
$$assets = \frac{shares \times AssetsAvailable}{totalShares}$$
Shares are burned and the proportional asset amount is released to the withdrawer.
### 7.5. Failure Conditions
Same as `VaultDeposit` (see Section 6.4), but referencing `on_withdraw` instead of `on_deposit`. Additional failure conditions:
- `setExchangeAmount` returned an asset amount of 0 or an amount exceeding `AssetsAvailable` (`tecWASM_REJECTED`).
### 7.6. Metadata Changes
Same as `VaultDeposit` (see Section 6.5), but referencing `on_withdraw`.
## 8. Transaction: `VaultSet`
The `VaultSet` transaction allows updating mutable vault fields. For Smart Vaults, the `Data` field size limit is increased from 256 bytes to 4KB (`maxWasmDataLength = 4096`), providing sufficient state storage for WASM logic.
Note: The `VaultCode` field itself is immutable after creation and cannot be updated via `VaultSet`.
### 8.1. Fields
We propose one additional field:
| Field Name | Required? | JSON Type | Internal Type | Description |
| :--------------------- | :-------- | :-------- | :------------ | :------------------------------------------------------------------------------------------------- |
| `ComputationAllowance` | | `number` | `UInt32` | The amount of gas the user is willing to pay for the execution of the `on_set` WASM function. Optional; the node validates whether it is required at execution time based on the vault's WASM exports. |
### 8.2. WASM Execution
If the vault has a `VaultCode` that exports an `on_set` function:
1. The node detects the `on_set` export by inspecting the WASM binary on-chain.
2. If `ComputationAllowance` is provided, the `on_set` function is executed with the specified gas limit.
3. If the function returns a value greater than 0, the `VaultSet` proceeds with its normal field updates.
4. If the function returns 0 or a negative value, the transaction fails with `tecWASM_REJECTED`.
5. If the WASM execution modifies the `Data` field (via `updateData`), the change is persisted.
6. `GasUsed` and `WasmReturnCode` are recorded in the transaction metadata.
If the vault's `VaultCode` does not export `on_set`, the `VaultSet` transaction proceeds with its default behavior (owner-only, standard field validation).
The submitter does not need to inspect the WASM binary to know whether `on_set` is exported. If they omit `ComputationAllowance` and the vault requires it, the node rejects the transaction with a clear error code (see Section 8.4). This allows clients to retry with `ComputationAllowance` if needed.
### 8.3. Transaction Fee
Same formula as `VaultDeposit` when `ComputationAllowance` is provided (see Section 6.2). Standard fee otherwise.
### 8.4. Failure Conditions
- The vault's `VaultCode` exports `on_set` but `ComputationAllowance` is not provided (`tefWASM_FIELD_NOT_INCLUDED`).
- `ComputationAllowance` is provided but the vault's `VaultCode` does not export `on_set` (`tefNO_WASM`).
- The `on_set` function returns 0 or a negative number (`tecWASM_REJECTED`).
## 9. Transaction: `VaultDelete`
The `VaultDelete` transaction already exists. The modifications for Smart Vaults:
When deleting a vault with `VaultCode`, the owner count is decreased by the variable reserve amount (matching the increase from `VaultCreate`). This ensures that the additional reserves for WASM code are properly returned.
### 9.1. Fields
We propose one additional field:
| Field Name | Required? | JSON Type | Internal Type | Description |
| :--------------------- | :-------- | :-------- | :------------ | :---------------------------------------------------------------------------------------------------- |
| `ComputationAllowance` | | `number` | `UInt32` | The amount of gas the user is willing to pay for the execution of the `on_delete` WASM function. Optional; the node validates whether it is required at execution time based on the vault's WASM exports. |
### 9.2. WASM Execution
If the vault has a `VaultCode` that exports an `on_delete` function:
1. The node detects the `on_delete` export by inspecting the WASM binary on-chain.
2. If `ComputationAllowance` is provided, the `on_delete` function is executed with the specified gas limit.
3. If the function returns a value greater than 0, the deletion proceeds with standard checks (e.g., zero outstanding shares).
4. If the function returns 0 or a negative value, the transaction fails with `tecWASM_REJECTED`.
5. `GasUsed` and `WasmReturnCode` are recorded in the transaction metadata.
Note: `updateData` is a no-op during `on_delete` execution, since the vault will be deleted if the function approves. The `setExchangeAmount` host function is not available during `on_delete`.
If the vault's `VaultCode` does not export `on_delete`, the `VaultDelete` transaction proceeds with its default behavior.
If the vault is in emergency mode (see Section 13.4), `on_delete` is bypassed — the owner can delete the vault without WASM execution once all shares have been redeemed via emergency withdrawal.
### 9.3. Transaction Fee
Same formula as `VaultDeposit` when `ComputationAllowance` is provided (see Section 6.2). Standard fee otherwise.
### 9.4. Failure Conditions
The existing failure conditions still apply. These failure conditions are added:
- The vault's `VaultCode` exports `on_delete` but `ComputationAllowance` is not provided (`tefWASM_FIELD_NOT_INCLUDED`).
- `ComputationAllowance` is provided but the vault's `VaultCode` does not export `on_delete` (`tefNO_WASM`).
- The `on_delete` function returns 0 or a negative number (`tecWASM_REJECTED`).
## 10. Fee Settings and Fee Voting
Smart Vaults reuse the same `FeeSettings` fields introduced by Smart Escrows, with one addition:
| Field | Description | Initial Value |
| :---------------------- | :---------------------------------------------------------------------- | :------------------ |
| `ExtensionComputeLimit` | The maximum amount of gas that one extension can execute. | 100,000 |
| `ExtensionSizeLimit` | The maximum size, in bytes, that an extension can be. | 100,000 |
| `GasPrice` | The cost of 1 gas, in micro-drops (1 millionth of a drop). | 1,000 |
| `EmergencyDelay` | The delay, in seconds, between emergency signal and activation. | 604,800 (7 days) |
These are voted on by validators and can be adjusted without a new amendment.
## 11. How the `VaultCode` Field Works
The `VaultCode` field contains compiled WebAssembly (WASM) code. The WASM engine and host function API are shared with Smart Escrows (detailed in a separate XLS).
### 11.1. WASM Function Signatures
The WASM module must export (required):
```
(func (export "on_deposit") (result i32))
(func (export "on_withdraw") (result i32))
```
The WASM module may optionally export:
```
(func (export "on_set") (result i32))
(func (export "on_delete") (result i32))
```
All functions take no parameters and return an `i32`:
- Return value > 0: operation is permitted
- Return value <= 0: operation is rejected (`tecWASM_REJECTED`)
### 11.2. Host Functions Available
The full host function API from Smart Escrows is available, including:
- **Ledger queries**: `getLedgerSqn`, `getParentLedgerTime`, `getParentLedgerHash`, `getBaseFee`
- **Amendment queries**: `isAmendmentEnabled`
- **Ledger object access**: `cacheLedgerObj`, `getTxField`, `getCurrentLedgerObjField`, `getLedgerObjField`
- **Nested field access**: `getTxNestedField`, `getCurrentLedgerObjNestedField`, `getLedgerObjNestedField`
- **Array access**: `getTxArrayLen`, `getCurrentLedgerObjArrayLen`, etc.
- **State mutation**: `updateData` (modify the vault's `Data` field)
- **Exchange control**: `setExchangeAmount` (set shares to mint or assets to release — Smart Vault only, `on_deposit`/`on_withdraw` only)
- **Crypto**: `checkSignature`, `computeSha512HalfHash`
- **Keylet computation**: `accountKeylet`, `vaultKeylet`, `escrowKeylet`, etc.
- **NFT queries**: `getNFT`, `getNFTIssuer`, etc.
- **Float arithmetic**: `floatAdd`, `floatMultiply`, `floatCompare`, etc.
- **BN254 elliptic curve** (for ZK proofs): `bn254AddHelper`, `bn254MulHelper`, `bn254NegHelper`, `bn254PairingHelper`
- **Trace/Debug**: `trace`, `traceNum`, `traceAccount`
#### Host Function to `xrpl_std` Mapping
The code examples in Section 15 use the `xrpl_std` Rust crate, which provides ergonomic wrappers around the raw host functions. The following table maps commonly used wrapper functions to their underlying host function calls:
| `xrpl_std` Function | Underlying Host Function(s) |
| :------------------------------ | :---------------------------------------------------------------- |
| `get_tx_amount()` | `getTxField` with the `Amount` field code |
| `get_tx_field::<T>(sfield)` | `getTxField` with the specified serialized field code |
| `get_vault_assets_total()` | `getCurrentLedgerObjField` with the `AssetsTotal` field code |
| `get_vault_shares_total()` | `getCurrentLedgerObjField` with the `ShareMPTID` field + MPT query |
| `get_current_data()` | `getCurrentLedgerObjField` with the `Data` field code |
| `update_data(&data)` | `updateData` host function |
| `set_exchange_amount(amount)` | `setExchangeAmount` host function |
| `credential_keylet(...)` | `credentialKeylet` host function |
| `bn254_pairing_helper(pairs)` | `bn254PairingHelper` host function |
### 11.3. Constraints
- Gas limits on execution (as specified in `ExtensionComputeLimit` in `FeeSettings`)
- Code size limits (as specified in `ExtensionSizeLimit` in `FeeSettings`)
- One 4KB `Data` field for state storage per vault
- Read-access of ledger objects allowed
- No write access to other ledger objects (only to the vault's `Data` field via `updateData`)
- No transaction emission / creation
## 12. Invariants
### 12.1. WASM Vault Invariants
Smart Vaults with `WithdrawalPolicy = vaultStrategyWASM` use shares (MPTokens) for ownership tracking, but the exchange rate is determined by the WASM code rather than a fixed formula. This affects invariant checks:
- The standard invariant "deposit assets and shares must change by proportional amounts" is relaxed for WASM vaults, since the WASM code controls the exchange rate.
- The "zero shares implies zero assets" invariant still applies — if all shares are redeemed, the vault must be empty.
- All other vault invariants (asset immutability, pseudo-account consistency, etc.) still apply.
- Shares must always change on deposit (minted) and withdrawal (burned) — the WASM code cannot skip share accounting.
#### 12.1.1. `setExchangeAmount` Invariants
The ledger enforces the following invariants on the value passed to `setExchangeAmount`, after WASM execution completes:
| Context | Invariant |
| :------------- | :------------------------------------------------------------------------------------------------- |
| `on_deposit` | `sharesAmount > 0` |
| `on_withdraw` | `assetAmount > 0` |
| `on_withdraw` | `assetAmount <= AssetsAvailable` |
If any invariant is violated, the transaction fails with `tecWASM_REJECTED`. These checks are enforced by the ledger, not the WASM code, providing a safety net against buggy or malicious extensions.
#### 12.1.2. Deposit Invariants
Deposits are all-or-nothing. The full `Amount` specified in the `VaultDeposit` transaction is transferred to the vault, or the transaction is rejected. The WASM code cannot partially accept a deposit.
### 12.2. General Invariants
- `VaultCode` is immutable after creation.
- If `VaultCode` is present, `WithdrawalPolicy` must be `vaultStrategyWASM`.
- The vault must always have a valid pseudo-account.
- `AssetsAvailable` must be >= 0 and <= `AssetsTotal`.
## 13. Security
### 13.1. Gas Metering
All WASM execution is gas-metered. The `ComputationAllowance` field on deposit/withdraw transactions sets the gas limit. If the WASM code exceeds the gas limit, execution halts and the transaction fails. The actual gas consumed is recorded in metadata.
### 13.2. Code Validation
On `VaultCreate`, the WASM code is validated:
1. It must be valid WASM bytecode.
2. It must export `on_deposit` and `on_withdraw` with the correct signatures.
3. It must not exceed the `ExtensionSizeLimit`.
### 13.3. Sandboxing
WASM code executes in a sandboxed environment with:
- No filesystem or network access
- Memory limited to 8MB (128 pages of 64KB)
- Only approved host functions available
- No ability to modify ledger state except the vault's own `Data` field
### 13.4. Emergency Withdrawal Mechanism
If a bug in the WASM code prevents withdrawals, depositors' funds would be permanently stuck. To guarantee fund recovery while preserving trust in the WASM logic, Smart Vaults include a **signal-and-delay circuit breaker**.
#### 13.4.1. How It Works
1. **Emergency signal**: The vault owner submits a `VaultSet` transaction with the `tfEmergencyWithdraw` flag. This records the current ledger close time in the `EmergencySignalTime` field on the vault.
2. **Grace period**: During the `EmergencyDelay` period (configurable via `FeeSettings`, default: 604,800 seconds = 7 days), normal WASM operations continue. Depositors can see the emergency signal on-chain and withdraw through the WASM code if they choose. The owner can cancel the signal during this period by clearing the `tfEmergencyWithdraw` flag via `VaultSet`.
3. **Emergency mode activation**: Once `EmergencyDelay` seconds have elapsed since `EmergencySignalTime`, the vault enters emergency mode. This is irreversible.
4. **Emergency mode behavior**: Once emergency mode is active:
- **No deposits**: All `VaultDeposit` transactions are rejected with `tecEMERGENCY_MODE`.
- **No WASM execution**: All WASM-based withdrawals are rejected. `ComputationAllowance` must not be provided.
- **Pro-rata withdrawals only**: Any account holding shares can submit a `VaultWithdraw` without `ComputationAllowance`. Shares are burned and the proportional amount of assets (`shares / totalShares * assetsAvailable`) is released.
- **WASM-free deletion**: Once all shares have been redeemed, the owner can delete the vault via `VaultDelete` without `ComputationAllowance`, bypassing the `on_delete` hook.
#### 13.4.2. New Vault Fields
| Field Name | Required? | JSON Type | Internal Type | Description |
| :-------------------- | :-------- | :-------- | :------------ | :------------------------------------------------------------------------- |
| `EmergencySignalTime` | | `number` | `UInt32` | Ledger close time when the owner signaled emergency withdrawal intent. Set automatically when `tfEmergencyWithdraw` is applied. Cleared if the flag is removed during the grace period. |
#### 13.4.3. New Vault Flag
| Flag | Value | Description |
| :--------------------- | :----------- | :------------------------------------------------------------- |
| `lsfEmergencyWithdraw` | `0x00020000` | Owner has signaled intent to activate emergency withdrawal mode. |
#### 13.4.4. Emergency Mode Fairness
Emergency mode uses pro-rata redemption based on on-chain share balances. For vaults that use a non-linear exchange rate (e.g., bonding curves), pro-rata distribution may not match what the WASM code would have returned. For example, if early depositors received more shares per asset due to a bonding curve, they will receive a proportionally larger share of the remaining assets in emergency mode.
**This is an inherent tradeoff.** The emergency mechanism prioritizes **fund recovery** over **exact fairness** according to the WASM code's pricing model. Depositors should be aware that:
- The emergency pro-rata distribution reflects their **share ownership**, not their original deposit amount.
- For vaults with non-linear pricing, this may result in a distribution that differs from what the WASM code would have computed.
- Despite this imprecision, the mechanism guarantees that funds are never permanently stuck and that the distribution is proportional to on-chain ownership records.
Vault creators should document their pricing model and the implications for emergency withdrawal so depositors can make informed decisions.
#### 13.4.5. Design Rationale
This mechanism ensures:
- **Depositor safety**: Funds are never permanently stuck. If the WASM code is broken, the delay countdown guarantees eventual recovery.
- **Grace period for normal exit**: During the delay, depositors can still withdraw through the WASM code if it's functional, getting the WASM-computed exchange rate rather than the pro-rata fallback.
- **Transparency**: The `lsfEmergencyWithdraw` flag and `EmergencySignalTime` are visible on-chain. Depositors can see that the owner has signaled emergency intent and have the full delay period to act.
- **No re-entry after activation**: Once emergency mode activates, the vault is frozen for new deposits. This prevents manipulation of the pool composition during emergency withdrawal.
- **Irreversibility**: Once emergency mode is active, it cannot be reversed. This simplicity prevents oscillation attacks where the owner toggles between modes.
#### 13.4.6. Comparison with Smart Escrows
Smart Escrows use a mandatory `CancelAfter` field as their safety mechanism — the escrow expires and funds return to the sender. Smart Vaults use the signal-and-delay circuit breaker instead, which is more appropriate for long-lived pooled assets that don't have a natural expiration.
### 13.5. Code Verifiability
Smart Vaults follow the same code verifiability model as Smart Escrows. The WASM bytecode is stored on-chain and is immutable after creation. Anyone can read and verify the `VaultCode` from the `Vault` ledger object before depositing. Since the code cannot be changed, the rules verified at deposit time are guaranteed to remain in effect for the lifetime of the vault. Off-chain tools can decompile or formally verify the WASM bytecode to audit vault logic.
## 14. Rationale
### 14.1. Design Choices
- **Reuse of Smart Escrow infrastructure**: Smart Vaults use the same WASM engine, host functions, gas metering, and fee voting mechanisms as Smart Escrows. This minimizes implementation complexity and provides a consistent developer experience.
- **WASM-controlled exchange rate with shares**: Shares (MPTokens) are always minted on deposit and burned on withdrawal, but the exchange rate is determined by the WASM code via `setExchangeAmount`. This enables custom pricing models (bonding curves, fees, fixed receipts) while maintaining on-chain ownership records that support trustless emergency withdrawal.
- **Two entry points**: Having separate `on_deposit` and `on_withdraw` functions (rather than a single gate function) allows the WASM code to implement asymmetric logic (e.g., anyone can deposit, but only specific accounts can withdraw).
- **Optional lifecycle hooks**: The `on_set` and `on_delete` hooks allow vault creators to add governance logic to vault updates and deletion without requiring them for simple use cases.
- **Immutable code**: Like Smart Escrows, the `VaultCode` cannot be updated after creation. This ensures depositors can trust the rules they deposited under won't change.
- **4KB Data field**: Provides sufficient state storage for most use cases (counters, allowlists, accumulated state) while keeping ledger bloat manageable.
### 14.2. Alternatives Considered
- **Modifiable VaultCode**: Allowing vault owners to update the WASM code was rejected because it would break trust guarantees for depositors.
- **No shares for WASM vaults**: Bypassing the share mechanism entirely was considered but rejected because shares provide trustless on-chain ownership records needed for the emergency withdrawal mechanism.
- **Separate Smart Vault object type**: Creating a new ledger object type was rejected in favor of extending the existing `Vault` with a new `WithdrawalPolicy`.
### 14.3. Relationship to Smart Escrows
| Feature | Smart Escrows | Smart Vaults |
| :------------- | :---------------------- | :------------------------------------------------------------------- |
| WASM field | `FinishFunction` | `VaultCode` |
| Entry point(s) | `finish` | `on_deposit`, `on_withdraw`, `on_set` (optional), `on_delete` (optional) |
| Trigger | `EscrowFinish` | `VaultDeposit`, `VaultWithdraw`, `VaultSet`, `VaultDelete` |
| Asset model | Hold-and-release | Pool-and-manage with WASM-controlled exchange rate |
| State storage | `Data` (4KB) | `Data` (4KB) |
| Gas field | `ComputationAllowance` | `ComputationAllowance` |
| Safety | Mandatory `CancelAfter` | Signal-and-delay emergency withdrawal |
| WASM engine | Shared (Wasmi) | Shared (Wasmi) |
| Host functions | Shared | Shared + `setExchangeAmount` |
| Amendment | `SmartEscrow` | `SmartVault` |
## 15. Code Examples
> **Note:** The code examples below are illustrative pseudocode intended to demonstrate the programming model. The `xrpl_std` Rust crate exists and provides ergonomic wrappers around the raw WASM host functions — the actual API may differ in function signatures, naming, or error handling. See Section 11.2 for the mapping between `xrpl_std` functions and the underlying host function API.
### 15.1. Standard Pro-Rata Vault (WASM Passthrough)
A vault that mimics standard FCFS behavior — useful as a starting template.
```rust
use xrpl_std::*;
#[no_mangle]
pub extern "C" fn on_deposit() -> i32 {
let deposit_amount = get_tx_amount();
let total_assets = get_vault_assets_total();
let total_shares = get_vault_shares_total();
// Standard pro-rata: shares = deposit * totalShares / totalAssets
let shares = if total_shares == 0 {
deposit_amount // first deposit: 1:1
} else {
deposit_amount * total_shares / total_assets
};
set_exchange_amount(shares);
1
}
#[no_mangle]
pub extern "C" fn on_withdraw() -> i32 {
let shares_to_burn = get_tx_amount(); // shares submitted
let total_assets = get_vault_assets_total();
let total_shares = get_vault_shares_total();
// Standard pro-rata: assets = shares * totalAssets / totalShares
let assets = shares_to_burn * total_assets / total_shares;
set_exchange_amount(assets);
1
}
```
### 15.2. Owner-Only Withdraw with Pro-Rata
A vault where anyone can deposit, but only the vault owner can withdraw.
```rust
use xrpl_std::*;
const OWNER: AccountID = /* vault owner address */;
#[no_mangle]
pub extern "C" fn on_deposit() -> i32 {
let deposit_amount = get_tx_amount();
// 1:1 shares for simplicity
set_exchange_amount(deposit_amount);
1 // Anyone can deposit
}
#[no_mangle]
pub extern "C" fn on_withdraw() -> i32 {
let tx_account = match get_tx_field::<AccountID>(sfield::Account) {
Ok(v) => v,
Err(e) => return e.code(),
};
if tx_account != OWNER { return 0; }
let shares_to_burn = get_tx_amount();
let total_assets = get_vault_assets_total();
let total_shares = get_vault_shares_total();
let assets = shares_to_burn * total_assets / total_shares;
set_exchange_amount(assets);
1
}
```
### 15.3. Credential-Gated Vault
A vault that only allows deposits from accounts holding a specific credential.
```rust
use xrpl_std::*;
const ISSUER: AccountID = /* credential issuer */;
const CRED_TYPE: &[u8] = b"kyc_verified";
#[no_mangle]
pub extern "C" fn on_deposit() -> i32 {
let tx_account = match get_tx_field::<AccountID>(sfield::Account) {
Ok(v) => v,
Err(e) => return e.code(),
};
match credential_keylet(&tx_account, &ISSUER, CRED_TYPE) {
Ok(keylet) => {
let slot = unsafe {
host::cache_ledger_obj(keylet.as_ptr(), keylet.len(), 0)
};
if slot < 0 { return 0; } // credential doesn't exist
let deposit_amount = get_tx_amount();
set_exchange_amount(deposit_amount); // 1:1 shares
1
}
Err(e) => e.code(),
}
}
#[no_mangle]
pub extern "C" fn on_withdraw() -> i32 {
let shares = get_tx_amount();
let total_assets = get_vault_assets_total();
let total_shares = get_vault_shares_total();
set_exchange_amount(shares * total_assets / total_shares);
1 // Anyone with shares can withdraw
}
```
### 15.4. Bonding Curve Vault
A vault where the share price increases with supply — early depositors get more shares per asset.
```rust
use xrpl_std::*;
// Price = BASE_PRICE + (SLOPE * current_supply)
const BASE_PRICE: u64 = 1_000_000; // 1 XRP in drops
const SLOPE: u64 = 100; // price increases 100 drops per share outstanding
#[no_mangle]
pub extern "C" fn on_deposit() -> i32 {
let deposit_amount = get_tx_amount();
let current_supply = get_vault_shares_total();
// Current price per share
let price = BASE_PRICE + SLOPE * current_supply;
let shares = deposit_amount * 1_000_000 / price;
if shares == 0 { return 0; } // deposit too small
set_exchange_amount(shares);
1
}
#[no_mangle]
pub extern "C" fn on_withdraw() -> i32 {
let shares_to_burn = get_tx_amount();
let current_supply = get_vault_shares_total();
// Price at current supply level
let price = BASE_PRICE + SLOPE * (current_supply - shares_to_burn);
let assets = shares_to_burn * price / 1_000_000;
set_exchange_amount(assets);
1
}
```
### 15.5. Privacy Pool (Simplified)
A vault that issues 1 receipt token per deposit and verifies a ZK proof on withdrawal. The merkle root of commitments is stored in the `Data` field.
> **Note:** This is a simplified illustration. The 4KB `Data` field limits practical merkle tree size — a binary merkle tree with 32-byte leaves and a nullifier bitmap can support on the order of ~100 deposits before exhausting storage. Production privacy pools may need to use accumulator-based schemes (e.g., a single 32-byte root updated incrementally) or store only the current root with proofs referencing off-chain data availability.
```rust
use xrpl_std::*;
const FIXED_DEPOSIT: u64 = 100_000_000; // 100 XRP in drops
#[no_mangle]
pub extern "C" fn on_deposit() -> i32 {
let deposit_amount = get_tx_amount();
if deposit_amount != FIXED_DEPOSIT { return 0; } // must deposit exact amount
// Read commitment from transaction memo
let commitment = get_tx_memo_data();
// Update merkle root in Data field
let mut data = get_current_data();
let new_root = compute_merkle_insert(&data, &commitment);
data[0..32].copy_from_slice(&new_root);
update_data(&data);
// Mint exactly 1 receipt token
set_exchange_amount(1);
1
}
#[no_mangle]
pub extern "C" fn on_withdraw() -> i32 {
// Read ZK proof and nullifier from transaction memo
let proof = get_tx_memo_data();
let nullifier = &proof[0..32];
let zk_pairs = &proof[32..];
// Check nullifier hasn't been used (stored as bitmap in Data)
let data = get_current_data();
if is_nullifier_used(&data, nullifier) { return 0; }
// Verify ZK proof via BN254 pairing
match bn254_pairing_helper(zk_pairs) {
Ok(1) => {
// Mark nullifier as used
let mut new_data = data.clone();
mark_nullifier_used(&mut new_data, nullifier);
update_data(&new_data);
// Release fixed amount, burn 1 receipt token
set_exchange_amount(FIXED_DEPOSIT);
1
}
_ => 0, // proof invalid
}
}
```
# Appendix
## Appendix A: FAQ
### A.1: How do Smart Vaults differ from Smart Escrows?
Smart Escrows control the **release** of a held payment (one-time event). Smart Vaults control **ongoing deposits and withdrawals** to a shared asset pool. Escrows are point-to-point; vaults are pool-based.
### A.2: Can an existing vault be upgraded to a Smart Vault?
No. The `VaultCode` and `WithdrawalPolicy` are set at creation and cannot be changed. To use Smart Vault functionality, a new vault must be created.
### A.3: Can the `VaultCode` be updated after creation?
No. This ensures that depositors can trust the rules under which they deposited their assets.
### A.4: Can the `Data` field be updated with a transaction?
The `Data` field can be updated in two ways:
1. By the WASM code during execution (via the `updateData` host function).
2. By the vault owner via a `VaultSet` transaction (subject to `on_set` if exported).
### A.5: How do WASM vaults use shares differently?
Standard vaults compute shares via a fixed pro-rata formula. WASM vaults always mint and burn shares, but the **exchange rate is determined by the WASM code** via the `setExchangeAmount` host function. This enables custom pricing (bonding curves, fee structures, fixed-amount receipt tokens) while maintaining on-chain ownership tracking. Shares serve as trustless proof of deposit, enabling emergency withdrawal if the WASM code has a bug.
### A.6: What happens if the WASM code has a bug?
Smart Vaults include a signal-and-delay emergency withdrawal mechanism (see Section 13.4). If the WASM code prevents all withdrawals:
1. The vault owner signals emergency intent via `VaultSet` with `tfEmergencyWithdraw`.
2. During the grace period (`EmergencyDelay`, default 7 days), normal WASM operations continue. Depositors can withdraw through the WASM code if it's functional.
3. After the grace period, emergency mode activates: no more deposits, no WASM execution, only pro-rata withdrawals.
This guarantees that funds are never permanently stuck, while giving depositors time to exit through the WASM code during the grace period.
### A.7: Can Smart Vaults interact with Smart Escrows?
Yes. The WASM code in a Smart Vault can read escrow ledger objects (using `cacheLedgerObj` with the escrow keylet) to condition vault operations on escrow state. However, WASM code cannot create, modify, or finish escrows.
### A.8: What is the maximum code size for `VaultCode`?
The maximum code size is determined by the `ExtensionSizeLimit` field in `FeeSettings`, initially set to 100,000 bytes (100KB). This can be adjusted by validator voting.
### A.9: How do I know if a vault's `VaultCode` exports `on_set` or `on_delete`?
You don't need to inspect the WASM binary yourself. Submit the transaction without `ComputationAllowance`. If the vault requires it (because the relevant hook is exported), the node rejects the transaction with `tefWASM_FIELD_NOT_INCLUDED`, and you can retry with `ComputationAllowance`.
## Appendix B: Amendment Dependencies
| Amendment | Status | Dependency |
| :----------------- | :------- | :-------------------------------------- |
| `SingleAssetVault` | Required | Base vault functionality |
| `WasmVM` | Required | WASM engine, host functions, fee voting |
| `SmartVault` | Proposed | This amendment |
## Appendix C: Open Questions
The following items are known areas for refinement and are included to invite focused feedback.
### C.1: `setExchangeAmount` Double-Call Behavior
The spec states that a second call to `setExchangeAmount` fails the transaction, but does not specify whether WASM execution is immediately trapped or continues with a failure flag checked after return. This needs to be pinned down for deterministic behavior across validator implementations.
### C.2: Emergency Mode and Owner Key Loss
The emergency withdrawal mechanism is owner-initiated only. If the owner's keys are lost, no one can trigger emergency mode and funds are stuck. If the keys are compromised, an attacker can trigger emergency mode to grief depositors. Should there be an alternative trigger mechanism (e.g., depositor-initiated with share-weighted voting)? At minimum, the spec should recommend multisig ownership for Smart Vaults.
### C.3: `Data` Field Size Limitations
At 4KB, the `Data` field is insufficient for several listed use cases at scale (e.g., allowlists beyond ~128 accounts, per-account rate limiting). The spec should be explicit about which use cases require off-chain data availability, or consider a mechanism for larger state storage with additional reserves.
### C.4: `on_set` / `on_delete` Export Discovery
The current approach (submit without `ComputationAllowance`, get rejected, retry) costs a failed transaction fee on every first interaction with an unknown vault. Adding flags on the `Vault` object (e.g., `lsfHasOnSet`, `lsfHasOnDelete`) set at creation time would allow clients to query this without trial-and-error.
### C.5: Share Transfer Implications
MPToken shares can be transferred between accounts. For credential-gated vaults, a non-credentialed account receiving shares via transfer can still withdraw since `on_withdraw` may not re-check credentials. The spec should clarify whether this is intentional and document the implications for vault designers.
### C.6: Integer Overflow in Exchange Rate Calculations
With XRP amounts up to ~10^17 drops and large share supplies, intermediate arithmetic in `u64` can overflow. Should the spec mandate use of the float arithmetic host functions for exchange rate calculations, or document overflow-safe patterns?