feat(vault): add caller-supplied minimum output bounds (price protection) - #604
feat(vault): add caller-supplied minimum output bounds (price protection)#604Emmycivity wants to merge 1 commit into
Conversation
- 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
|
@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! 🚀 |
|
@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
left a comment
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
Overview
Adds caller-supplied minimum output bounds (
min_shares_outondepositandmin_amount_outonwithdraw) to theMeridianVaultcontract 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 errorContractError::SlippageExceeded(code 15). Unbounded callers can pass0to opt out of minimum enforcement.Related Issue
Closes #559
Changes
Vault Smart Contract
packages/contracts/vault/src/lib.rsContractError::SlippageExceeded = 15without renumbering existing error codes.pub fn deposit(env: Env, caller: Address, amount: i128, min_shares_out: i128) -> Result<i128, ContractError>to checkshares_to_mint >= min_shares_out.pub fn withdraw(env: Env, caller: Address, shares: i128, min_amount_out: i128) -> Result<i128, ContractError>to checkusdc_out >= min_amount_out.deposit_enforces_min_shares_out_success,deposit_fails_when_shares_below_min_shares_out,withdraw_enforces_min_amount_out_success, andwithdraw_fails_when_amount_below_min_amount_out.&0_i128.SDK & Helpers
packages/stellar-sdk-helpers/src/coordinator.tsbuildCoordinatorDepositTxto acceptminSharesOut: bigint = 0nand pass 3 ScVal arguments (caller,amount,min_shares_out).buildCoordinatorWithdrawTxto acceptminAmountOut: bigint = 0nand pass 3 ScVal arguments (caller,shares,min_amount_out).packages/stellar-sdk-helpers/src/orchestration.tsbuildDepositTxandbuildWithdrawTxto accept optionalminSharesOutandminAmountOutstring arguments and convert to stroops.packages/stellar-sdk-helpers/src/coordinator.test.ts&packages/stellar-sdk-helpers/src/orchestration.test.tsShared Schemas & API
packages/shared/src/schemas.ts&packages/shared/src/schemas.test.tsminSharesOutandminAmountOutstring regex fields toDepositRequestSchemaandWithdrawRequestSchema.packages/api-core/src/tx.ts&packages/api-core/src/tx.test.tsminSharesOutandminAmountOutfrom request bodies intobuildDepositTxandbuildWithdrawTx.api/__tests__/handlers.test.tsminSharesOutandminAmountOut.Web Frontend & Documentation
apps/web/src/lib/api.ts&apps/web/src/hooks/useVaultActions.ts&apps/web/src/__tests__/hooks/useVaultActions.test.tsuseVaultActionshook to support optionalminSharesOut/minAmountOut.apps/docs/architecture/vault-contract.md&apps/docs/overview/how-it-works.mddepositandwithdrawminimum bound parameters andSlippageExceeded = 15error code in the contract error table.Verification Results
depositandwithdrawaccept minimum-output boundsmin_shares_out/min_amount_outSlippageExceedederror variant (code 15) added toContractErrorSlippageExceededwhen output falls below minimumdeposit_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_outvault-contract.mdandhow-it-works.mdupdated