Skip to content

Savings Vault Intents

Overview

Savings Vault Intents is a request-based withdrawal contract for Spark Savings Vaults V2. It is designed for large redemptions where the vault may not hold enough idle liquidity to satisfy an immediate redeem() call.

Instead of redeeming directly from the vault, a user creates an onchain withdrawal request that specifies the vault, the number of shares to redeem, the recipient of the underlying assets, and an expiry deadline. The Spark ALM Planner monitors these requests offchain, prepares liquidity for the target vault, and then fulfills the request atomically.

The contract never takes custody of user shares or redeemed assets. On fulfillment it calls vault.redeem(shares, recipient, account) so the underlying assets are sent directly to the designated recipient.

Supported Networks and Contract Addresses

NetworkContractAddress
EthereumSavingsVaultIntents0x592B7DB9906E6f8924C4D74c2A0aB86CE44fDDDf

Contract Details

Roles and Access Control

The contract uses OpenZeppelin AccessControlEnumerable and defines two roles:

  • DEFAULT_ADMIN_ROLE: Can update the max request deadline window and manage per-vault configuration.
  • RELAYER: Can fulfill pending requests after liquidity has been prepared offchain by the Spark ALM Planner.

Core State

  • RELAYER: Role identifier used for fulfillment permissions.
  • maxDeadlineDuration: Maximum time window a request deadline can be set into the future.
  • vaultConfig: Mapping of vault address to whitelist status and min/max asset thresholds.
  • vaultRequestCount: Per-vault request counter used to assign monotonically increasing request IDs.
  • withdrawRequests: Mapping of account => vault => WithdrawRequest for the currently active request.

Vault Configuration

Each supported vault has a VaultConfig:

  • whitelisted: Whether requests can be created for the vault.
  • minIntentAssets: Minimum underlying asset value allowed per request.
  • maxIntentAssets: Maximum underlying asset value allowed per request.

The mainnet production deployment was initialized for the following Spark Vaults V2 markets:

VaultAddressMin Intent AssetsMax Intent Assets
spUSDC0x28B3a8fb53B741A8Fd78c0fb9A6B2393d896a43d5,000,000 USDC500,000,000 USDC
spETH0xfE6eb3b609a7C8352A241f7F3A21CEA4e9209B8f1,250 ETH250,000 ETH
spPYUSD0x80128DbB9f07b93DDE62A6daeadb69ED14a7D3545,000,000 PYUSD500,000,000 PYUSD
spUSDT0xe2e7a17dFf93280dec073C995595155283e3C3725,000,000 USDT500,000,000 USDT

Request Lifecycle

  1. The user approves the Savings Vault Intents contract to spend their Spark Vault shares.
  2. The user calls request(vault, shares, recipient, deadline).
  3. The contract validates the vault configuration, the deadline, and the caller's balance and allowance.
  4. A RequestCreated event is emitted for the ALM Planner to pick up offchain.
  5. The ALM Planner prepares liquidity for the target Spark Vault.
  6. An address with the RELAYER role calls fulfill(account, vault, requestId).
  7. The contract deletes the stored request and calls vault.redeem(shares, recipient, account).

Fulfillment is atomic. The request is either redeemed in full or the transaction reverts.

Functions

Admin Functions

  • setMaxDeadlineDuration(uint256 maxDeadlineDuration_): Updates the maximum allowed request deadline window. The value cannot be zero.
  • updateVaultConfig(address vault, bool whitelisted_, uint256 minIntentAssets_, uint256 maxIntentAssets_): Updates whitelist status and the min/max intent bounds for a vault. minIntentAssets_ must be strictly less than maxIntentAssets_.

User Functions

  • request(address vault, uint256 shares, address recipient, uint256 deadline): Creates or overwrites the caller's pending request for a vault and returns a new requestId.
  • cancel(address vault): Cancels the caller's active request for a vault and returns the cancelled requestId.

Relayer Function

  • fulfill(address account, address vault, uint256 requestId): Fulfills a pending request by redeeming the user's shares from the vault and sending assets to the configured recipient. Only callable by the RELAYER role.

View Functions

  • maxDeadlineDuration(): Returns the maximum deadline offset in seconds.
  • vaultConfig(address vault): Returns the current whitelist flag and min/max thresholds for a vault.
  • vaultRequestCount(address vault): Returns the total number of requests ever created for a vault.
  • withdrawRequests(address account, address vault): Returns the currently active request for a given user and vault.
  • RELAYER(): Returns the role hash for the relayer role.

Request Validation

request() enforces the following preconditions:

  • The vault must be whitelisted.
  • The recipient must not be the zero address.
  • convertToAssets(shares) must be within the vault's configured min/max bounds.
  • The deadline must be strictly in the future and no greater than block.timestamp + maxDeadlineDuration.
  • The caller must hold at least shares vault shares.
  • The caller must have approved at least shares to the Savings Vault Intents contract.

Events Emitted

  • RequestCreated(address indexed account, address indexed vault, uint256 indexed requestId, uint256 shares, address recipient, uint256 deadline): Emitted when a request is created or overwritten.
  • RequestCancelled(address indexed account, address indexed vault, uint256 indexed requestId): Emitted when a request is cancelled.
  • RequestFulfilled(address indexed account, address indexed vault, uint256 indexed requestId): Emitted when a request is fulfilled.
  • VaultConfigUpdated(address indexed vault, bool indexed whitelisted, uint256 minIntentAssets, uint256 maxIntentAssets): Emitted when admin updates a vault's configuration.
  • MaxDeadlineDurationUpdated(uint256 indexed maxDeadlineDuration): Emitted when admin updates the maximum deadline window.

Custom Errors

  • VaultNotWhitelisted: The vault is not enabled for the intent flow.
  • InvalidRecipientAddress: The recipient is the zero address.
  • IntentAssetsBelowMin: The requested redemption is below the configured minimum.
  • IntentAssetsAboveMax: The requested redemption exceeds the configured maximum.
  • InvalidDeadline: The request deadline is invalid or exceeds the allowed window.
  • InsufficientShares: The caller does not hold enough vault shares.
  • InsufficientAllowance: The contract does not have enough share allowance.
  • RequestNotFound: No matching pending request exists for the account and vault.
  • DeadlineExceeded: The relayer attempted to fulfill a request after expiry.
  • InvalidAdminAddress, InvalidRelayerAddress, InvalidVaultAddress, InvalidMaxDeadlineDuration, InvalidIntentAmountBounds: Configuration errors.

Gotchas / Integration Concerns

One Active Request Per Vault

Each user can have only one active request per vault. Creating a new request for the same vault overwrites the existing one and assigns a new requestId.

Overwrites Do Not Emit a Cancel Event

Replacing an existing request via request() silently removes the old request. Integrators should treat the latest RequestCreated event for a given (account, vault) pair as authoritative.

No Partial Fills

fulfill() redeems the full requested share amount or reverts. The contract does not support partial fulfillment.

Balance and Allowance Can Change After Request Creation

Users can revoke allowance or move shares away after creating a request. In that case fulfillment will revert when the relayer eventually calls redeem().

Request IDs Protect Against Stale Fulfills

fulfill() requires the expected requestId, preventing a relayer from accidentally fulfilling a request that has since been cancelled or overwritten.

Additional Resources