Repository navigation
[simplex]: refuse vault batches on undelegated wallets, honour maxRedeem, bound bid permits, check Binance deposits - #1411
Conversation
Wizdave97
left a comment
There was a problem hiding this comment.
Verdict: approve with minor comments.
All four fixes match their descriptions:
- Mode
0x02packs the permit deadline asvalidUntilwithvalidAfter = 0, andSolverAccount.validateUserOpreturns no time range of its own. - Each bid signs a fresh permit, so no permit is reused.
- Sends, sweeps and delegation keep the 1 h deadline, and the EIP-2612 bootstrap path is untouched.
isDelegatedisn't cached.- An undelegated operator can still exit through a single-call vault-share send, i.e. a
redeemsent from the EOA. - A vault-share "send" is a
redeem, somaxRedeemis the right bound. - Binance's contract addresses match the SDK config for ETH, BSC, MATIC, ARBITRUM and BASE USDC.
-
The permit deadline uses the host's wall clock, not chain time (paymaster/provider/simplex.ts:320; the window is set at ContractInteractionService.ts:876-878).
- Suppose simplex runs on an operator desktop whose clock is D seconds behind the chain and the bundler. Each bid is then admissible for about
450 − D − bundler bufferseconds. - At around 6–7 minutes of skew (with the default 300 s validity), every sponsored bid fails validation, while the solver still pays Hyperbridge's bid deposit and fee. Before this PR, up to about 1 h of skew was tolerated.
- Suggestion: anchor the deadline to the destination head block's
timestamp.bidValidUntilBlockalready reads the head, so it could usegetBlockand pass an absolute deadline. At minimum, warn at boot when the host clock and the chain head differ by more than about 30 s.
- Suppose simplex runs on an operator desktop whose clock is D seconds behind the chain and the bundler. Each bid is then admissible for about
-
The fix is narrower than "rejected at validation, at no cost" (ContractInteractionService.ts:871-875, the PR body and the changelog).
- The permit expires about 120 s after the block window ends, and later still when blocks run faster than measured.
- An op included in that gap still passes validation, reverts with
FillExpired, and the solver pays the gas. The realistic remaining cases are the window closing between simulation and inclusion, and a placer submitting late on purpose. - So this shrinks the griefing window from about 1 h to about 2 min; it doesn't close it. Please reword it as "ops more than ~120 s past the bid window are rejected at validation".
- A complete fix would have
SolverAccountreturn the fill'svalidUntilas a block number, since EntryPoint v0.9 accepts block-number validity ranges. That's a contract change for a follow-up.
-
The capped-redeem log promises a later pass that never runs. VaultFundingPlanner.ts:639 logs "the rest is left for a later pass", but
redeemAllonly runs at shutdown or onPOST /api/vault/redeem. Say instead that the operator has to redeem the rest.
Notes:
- Unsponsored bids (#1418's
assertUnsponsoredBidFundedpath) carry no Permit2 deadline, so this bound covers sponsored bids only. - #1412 adds the same
isDelegatedrefusal for native multi-callTokenSenderbatches. Together the two PRs cover both nativeexecuteBatchpaths; a shared helper would keep them consistent. - #1422's
RETRACTED_BID_MARGIN_SECONDS = 60should reuseBID_PERMIT_SKEW_SECONDS. - The conflicts with #1423 in
bid-budget.test.tsare mechanical. - The
Files:/Tests:list at the end of the changelog note repeats the diff and can go.
- Anchor a bid's Permit2 deadline to the destination head's timestamp, not the host clock. `permitDeadlineSeconds` becomes an absolute `permitDeadline`; `prepareBidUserOp` reads the head once with `getBlock` and reuses its number for `bidValidUntilBlock`. Other ops keep the host clock plus one hour. Test: a host 400s behind the chain still yields head + window. - Reword the guarantee: ops well past the bid window are rejected at validation; one landing in the tail still reverts `FillExpired` at the solver's cost. A full fix needs a block-number `validUntil` from `SolverAccount` (noted as a contract follow-up). - The capped-redeem warning says the operator has to redeem the rest: `redeemAll` runs only at shutdown or on POST /api/vault/redeem. - Drop the Files/Tests lists from the changelog note and revise it.
Fold the capped-redeem warning into the maxRedeem section so the note describes the final behaviour once.
- Anchor a bid's Permit2 deadline to the destination head's timestamp, not the host clock. `permitDeadlineSeconds` becomes an absolute `permitDeadline`; `prepareBidUserOp` reads the head once with `getBlock` and reuses its number for `bidValidUntilBlock`. Other ops keep the host clock plus one hour. Test: a host 400s behind the chain still yields head + window. - Reword the guarantee: ops well past the bid window are rejected at validation; one landing in the tail still reverts `FillExpired` at the solver's cost. A full fix needs a block-number `validUntil` from `SolverAccount` (noted as a contract follow-up). - The capped-redeem warning says the operator has to redeem the rest: `redeemAll` runs only at shutdown or on POST /api/vault/redeem. - Drop the Files/Tests lists from the changelog note and revise it.
Fold the capped-redeem warning into the maxRedeem section so the note describes the final behaviour once.
f5e099b to
86927f6
Compare
Wizdave97
left a comment
There was a problem hiding this comment.
Approved. The permit deadline now comes from the destination chain's head block timestamp, using a block read that already happens, so there's no extra RPC call. The capped-redeem log is fixed.
Non-blocking follow-ups:
- Flow doc. paymaster-selection-for-a-sponsored-userop.md:12 still describes
deadline = now + permitDeadlineSeconds, an option that no longer exists. - PR description. It still says a late bid is rejected "at no cost" and still names
permitDeadlineSeconds. - Wrong revert name. The comment at ContractInteractionService.ts:878 says a late bid "reverted
Expired". It revertsFillExpired. - Changelog. Lines 43-45 describe this PR's first version (host clock plus a window) rather than main. State the rule directly instead.
…eem, bound bid permits, check Binance deposits
- Anchor a bid's Permit2 deadline to the destination head's timestamp, not the host clock. `permitDeadlineSeconds` becomes an absolute `permitDeadline`; `prepareBidUserOp` reads the head once with `getBlock` and reuses its number for `bidValidUntilBlock`. Other ops keep the host clock plus one hour. Test: a host 400s behind the chain still yields head + window. - Reword the guarantee: ops well past the bid window are rejected at validation; one landing in the tail still reverts `FillExpired` at the solver's cost. A full fix needs a block-number `validUntil` from `SolverAccount` (noted as a contract follow-up). - The capped-redeem warning says the operator has to redeem the rest: `redeemAll` runs only at shutdown or on POST /api/vault/redeem. - Drop the Files/Tests lists from the changelog note and revise it.
Fold the capped-redeem warning into the maxRedeem section so the note describes the final behaviour once.
18ac538 to
9a72d5c
Compare
This PR makes four fixes from the 2026-10-15 simplex audit (c14, c53, c12, c63).
Vault batches on an undelegated wallet (
src/funding/vault/VaultFundingPlanner.ts,src/core/boot.ts)executeBatchto the solver EOA. With no 7702 code that tx succeeds and does nothing, so a sweep recorded movements that never happened and burned gas every interval.submitBatchrefuses the native send whenisDelegatedis false, with the errorThe wallet is not 7702-delegated on <chain> (or its delegation could not be read); a vault batch sent to it would do nothing.onTx.POST /api/vault/sweepreturns it.maxRedeem(VaultFundingPlanner.redeemChain,TokenSender.send, the ERC-4626 ABI)min(balanceOf, maxRedeem), skips a vault whose cap is zero, and logs what is left behind. An operator vault-share send abovemaxRedeemis refused before anything is submitted.Bid permit deadline (
src/services/paymaster/*,ContractInteractionService.prepareBidUserOp)SimplexPaymasterreports the permit deadline as the op'svalidUntil. A bid that is valid for ~330 s and selected late therefore still passed validation, then revertedFillExpiredat the solver's expense.PaymasterOptions.permitDeadlineis an optional absolute deadline in unix seconds, passed throughbuildPaymasterAndDataintobuildPermit2Mode.bidValiditySeconds+BID_DISCOVERY_PAD_SECONDS(30 s) +BID_PERMIT_SKEW_SECONDS(120 s).prepareBidUserOpreads the head once withgetBlockand reuses its number forbidValidUntilBlock.validUntilis close to now.permitDeadline(sends, sweeps, delegation) signs the host clock plus one hour, as before. The EIP-2612 bootstrap mode is unchanged (its deadline ismaxUint256).validUntilis block-based, so an op that lands after it but before the permit deadline still revertsFillExpiredat the solver's cost; closing that needsSolverAccountto return a block-numbervalidUntil, which is a contract change. When blocks are slow, whichever limit comes first applies.Binance deposit preflight (
src/services/rebalancers/binance.ts)contractAddressfor the coin and network must match the token the transfer sends (case-insensitive,0xoptional);Validation
funding/vault,rebalancers/binance.preflight,services/SimplexPaymaster,services/bid-budget,token-sender): 5 files, 111 passed.SimplexPaymaster.test.ts: the signed deadline is the absolute value passed in, and the host clock plus one hour when none is passed;bid-budget.test.ts: a bid's deadline is the head timestamp plus the window, even with the host 400 s behind the chain.pnpm --filter="@hyperbridge/simplex":lintexit 0 (warnings only);test:data146 passed;test:unit261 passed;test:filler70 passed, 5 skipped.npx tsc --noEmit -p .: 14 errors, the ones already onmain; none in files this PR changes.evm/src/utils/SimplexPaymaster.sol: mode0x02packs the permit deadline asvalidUntil.Not covered: no mainnet run.