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
| Network | Contract | Address |
|---|---|---|
| Ethereum | SavingsVaultIntents | 0x592B7DB9906E6f8924C4D74c2A0aB86CE44fDDDf |
Contract Details
- Contract Name:
SavingsVaultIntents.sol - Contract Source: Spark Savings Intents code repository
- Ethereum Deployment: 0x592B7DB9906E6f8924C4D74c2A0aB86CE44fDDDf
- Initial
maxDeadlineDuration:604800seconds (7 days)
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 ofaccount => vault => WithdrawRequestfor 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:
| Vault | Address | Min Intent Assets | Max Intent Assets |
|---|---|---|---|
| spUSDC | 0x28B3a8fb53B741A8Fd78c0fb9A6B2393d896a43d | 5,000,000 USDC | 500,000,000 USDC |
| spETH | 0xfE6eb3b609a7C8352A241f7F3A21CEA4e9209B8f | 1,250 ETH | 250,000 ETH |
| spPYUSD | 0x80128DbB9f07b93DDE62A6daeadb69ED14a7D354 | 5,000,000 PYUSD | 500,000,000 PYUSD |
| spUSDT | 0xe2e7a17dFf93280dec073C995595155283e3C372 | 5,000,000 USDT | 500,000,000 USDT |
Request Lifecycle
- The user approves the Savings Vault Intents contract to spend their Spark Vault shares.
- The user calls
request(vault, shares, recipient, deadline). - The contract validates the vault configuration, the deadline, and the caller's balance and allowance.
- A
RequestCreatedevent is emitted for the ALM Planner to pick up offchain. - The ALM Planner prepares liquidity for the target Spark Vault.
- An address with the
RELAYERrole callsfulfill(account, vault, requestId). - 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 thanmaxIntentAssets_.
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 newrequestId.cancel(address vault): Cancels the caller's active request for a vault and returns the cancelledrequestId.
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 theRELAYERrole.
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
sharesvault shares. - The caller must have approved at least
sharesto 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.