Skip to content

feat(vault): add caller-supplied minimum output bounds (price protection) - #604

Open
Emmycivity wants to merge 1 commit into
drydocs:mainfrom
Emmycivity:feat/issue-559-price-bounds
Open

feat(vault): add caller-supplied minimum output bounds (price protection)#604
Emmycivity wants to merge 1 commit into
drydocs:mainfrom
Emmycivity:feat/issue-559-price-bounds

Conversation

@Emmycivity

Copy link
Copy Markdown

Overview

Adds caller-supplied minimum output bounds (min_shares_out on deposit and min_amount_out on withdraw) to the MeridianVault contract to guard against sandwich attacks, frontrunning, and stale pricing across block execution. If the minted shares or returned assets fall below the caller-specified minimum bound, the contract reverts with typed error ContractError::SlippageExceeded (code 15). Unbounded callers can pass 0 to opt out of minimum enforcement.

Related Issue

Closes #559

Changes

Vault Smart Contract

  • [MODIFY] packages/contracts/vault/src/lib.rs
    • Added ContractError::SlippageExceeded = 15 without renumbering existing error codes.
    • Updated pub fn deposit(env: Env, caller: Address, amount: i128, min_shares_out: i128) -> Result<i128, ContractError> to check shares_to_mint >= min_shares_out.
    • Updated pub fn withdraw(env: Env, caller: Address, shares: i128, min_amount_out: i128) -> Result<i128, ContractError> to check usdc_out >= min_amount_out.
    • Added unit tests deposit_enforces_min_shares_out_success, deposit_fails_when_shares_below_min_shares_out, withdraw_enforces_min_amount_out_success, and withdraw_fails_when_amount_below_min_amount_out.
    • Updated existing contract tests to supply minimum bound &0_i128.

SDK & Helpers

  • [MODIFY] packages/stellar-sdk-helpers/src/coordinator.ts
    • Updated buildCoordinatorDepositTx to accept minSharesOut: bigint = 0n and pass 3 ScVal arguments (caller, amount, min_shares_out).
    • Updated buildCoordinatorWithdrawTx to accept minAmountOut: bigint = 0n and pass 3 ScVal arguments (caller, shares, min_amount_out).
  • [MODIFY] packages/stellar-sdk-helpers/src/orchestration.ts
    • Updated buildDepositTx and buildWithdrawTx to accept optional minSharesOut and minAmountOut string arguments and convert to stroops.
  • [MODIFY] packages/stellar-sdk-helpers/src/coordinator.test.ts & packages/stellar-sdk-helpers/src/orchestration.test.ts
    • Added test coverage for default and explicit minimum bounds.

Shared Schemas & API

  • [MODIFY] packages/shared/src/schemas.ts & packages/shared/src/schemas.test.ts
    • Added optional minSharesOut and minAmountOut string regex fields to DepositRequestSchema and WithdrawRequestSchema.
  • [MODIFY] packages/api-core/src/tx.ts & packages/api-core/src/tx.test.ts
    • Forwarded minSharesOut and minAmountOut from request bodies into buildDepositTx and buildWithdrawTx.
  • [MODIFY] api/__tests__/handlers.test.ts
    • Added tests for route handlers forwarding minSharesOut and minAmountOut.

Web Frontend & Documentation

  • [MODIFY] apps/web/src/lib/api.ts & apps/web/src/hooks/useVaultActions.ts & apps/web/src/__tests__/hooks/useVaultActions.test.ts
    • Updated web API client and useVaultActions hook to support optional minSharesOut / minAmountOut.
  • [MODIFY] apps/docs/architecture/vault-contract.md & apps/docs/overview/how-it-works.md
    • Documented deposit and withdraw minimum bound parameters and SlippageExceeded = 15 error code in the contract error table.

Verification Results

cargo test (packages/contracts):
  test result: ok. 58 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.66s
  test result: ok. 16 passed (meridian_blend_adapter)
  test result: ok. 13 passed (meridian_defindex_adapter)

pnpm test (workspace):
  Test Files  35 passed (35)
  Tests       422 passed (422)

pnpm typecheck:
  8 successful packages

pnpm lint:
  All packages passed

pnpm format:check:
  All matched files use Prettier code style!
Acceptance Criteria Status
deposit and withdraw accept minimum-output bounds ✅ Added min_shares_out / min_amount_out
SlippageExceeded error variant (code 15) added to ContractError ✅ Enum updated without renumbering existing codes
Reverts with SlippageExceeded when output falls below minimum ✅ Enforced and verified via test doubles
Passing 0 maintains unbounded behavior ✅ Defaults to 0 across SDK and test cases
Unit tests for accepted and rejected paths deposit_enforces_min_shares_out_success, deposit_fails_when_shares_below_min_shares_out, withdraw_enforces_min_amount_out_success, withdraw_fails_when_amount_below_min_amount_out
Architecture and overview documentation updated vault-contract.md and how-it-works.md updated

- Update MeridianVault deposit and withdraw to take min_shares_out and min_amount_out
- Add ContractError::SlippageExceeded (error code 15)
- Forward minSharesOut and minAmountOut across SDK, shared schemas, API core, and frontend
- Add comprehensive contract and unit tests for accepted and rejected bounds
- Update documentation in architecture and overview
@drips-wave

drips-wave Bot commented Aug 26, 2026

Copy link
Copy Markdown

@Emmycivity Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@vercel

vercel Bot commented Aug 26, 2026

Copy link
Copy Markdown

@Emmycivity is attempting to deploy a commit to the Collins' projects Team on Vercel.

A member of the Team first needs to authorize it.

@collinsezedike collinsezedike left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not anchorable since apps/web/src/components/dashboard/VaultPanel.tsx isn't touched by this PR: it's the only production call site for deposit()/withdraw() (via useVaultActions.ts), and it never passes minSharesOut/minAmountOut, e.g. await deposit(amount, bestVault.id, bestVault.asset) with no fourth argument. Every real deposit/withdrawal a user makes goes through with an unbounded 0 floor, so the sandwich/frontrunning protection this PR adds to close #559 never actually reaches production, only a caller who manually supplies the new argument gets any protection. The SDK/hook plumbing (useVaultActions.ts, api.ts, tx.ts, orchestration.ts) is wired correctly end to end otherwise; this is purely the UI never using it.

Separately: this PR is based on main from before #600 merged, and both changes touch the same ground. SlippageExceeded = 15 collides with #600's already-merged MinAmountOutNotMet = 15, and withdraw()'s new min_amount_out parameter duplicates #600's already-merged min_usdc_out parameter on the same function. After rebasing, the withdraw()-side changes here should be dropped, #600 already covers that half, leaving this PR scoped to just the deposit()-side min_shares_out addition, which is the genuinely new part.

/// `migrate_adapter` was called with `max_slippage_bps > 10_000`.
InvalidSlippageBps = 14,
/// `deposit` or `withdraw` output fell below caller-specified minimum amount.
SlippageExceeded = 15,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Collides with #600's already-merged MinAmountOutNotMet = 15 (this PR predates that merge). Needs renumbering after rebase.

env: Env,
caller: Address,
shares: i128,
min_amount_out: i128,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This duplicates #600's already-merged min_usdc_out parameter on withdraw() (same purpose, different name). After rebasing onto current main, drop the withdraw()-side changes here and keep this PR scoped to deposit()'s min_shares_out, which #600 doesn't cover.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Add caller-supplied price bounds to vault deposit/withdraw

2 participants