Skip to content

Allow unfunded orders with approval#4536

Open
fleupold wants to merge 6 commits into
mainfrom
allow-zero-balance-orders-with-approval
Open

Allow unfunded orders with approval#4536
fleupold wants to merge 6 commits into
mainfrom
allow-zero-balance-orders-with-approval

Conversation

@fleupold

@fleupold fleupold commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Description

There are use cases in which we want to allow limit order placement even if no funds are available yet. The existing logic requires the user to have at least 1 wei of balance. Reason for this is spam protection (funding a wallet requires a transaction and thus someone to spend gas). However, the same applies to setting an on-chain allowance.

This PR adds logic to allow completely unfunded accounts to place limit orders as long as they have set an onchain approval. Note that the cost of doing this is roughly equivalent to transferring 1 wei of token into an account (and then using a free permit pre-hook to set allowance, which would pass today's test). Therefore, I don't expect the spam surface to increase.

Changes

  • When validating an order and not using a full balance check, we now have a branch if transfer fails with insufficient balance, which checks if there exists on-chain approval (without evaluating any pre hooks or wrappers)
  • Add and implement allowance method on the balance fetcher
  • Adjust and add a test covering this path

How to test

Added unit test and adapted e2e test

Additional Context

Context: https://nomevlabs.slack.com/archives/C0A9GFVLLD6/p1781605451078769

@fleupold fleupold requested a review from a team as a code owner June 19, 2026 12:00
@claude

claude Bot commented Jun 19, 2026

Copy link
Copy Markdown

Claude finished @fleupold's task in 3m 0s —— View job


PR Review: Allow unfunded orders with approval

The approach is sound. The new branch only relaxes the non-full_balance_check path, only fires on InsufficientBalance (so InsufficientAllowance/TransferFailed still reject as before), and deliberately reads the standing on-chain allowance without applying pre-hooks — which keeps the spam-protection guarantee intact, as argued in the description. The External/Internal allowance logic mirrors tradable_balance_simple, and the e2e test correctly proves both the rejection (no approval) and acceptance (approval present) paths.

I left 3 inline comments (one minor logging suggestion + two doc/typo nits). Nothing blocking.

One design question worth a sanity check (not posting inline):

  • This relaxation applies to every order that doesn't request full_balance_check, not just limit orders — including market orders. A zero-balance market order with a standing approval would now be accepted and sit unfillable until funded. That's harmless if the autopilot's solvable-orders balance filtering keeps it out of auctions until funded (which I believe it does), but worth confirming that's the intended scope vs. limit-orders-only.

Minor reuse note: simulation.rs::allowance duplicates the source-handling logic already in tradable_balance_simple; could be factored out, but not necessary.

  • Read changed files and gather context
  • Analyze balance/allowance logic in order_validation.rs
  • Review simulation.rs allowance implementation
  • Review caching changes
  • Review e2e test changes
  • Post inline comments and summary

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces an allowance method to the BalanceFetching trait and its implementations to retrieve a user's on-chain allowance. The OrderValidator is updated to accept zero-balance orders if they are backed by a sufficient pre-existing allowance, and corresponding tests are added. The reviewer suggests propagating RPC errors from the allowance call instead of swallowing them with unwrap_or(U256::ZERO), which could otherwise mask transient network issues as validation errors.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread crates/shared/src/order_validation.rs Outdated
Comment thread crates/shared/src/order_validation.rs Outdated
Comment thread crates/shared/src/order_validation.rs Outdated
Comment thread crates/account-balances/src/lib.rs
fleupold and others added 5 commits June 19, 2026 14:06
@fleupold

Copy link
Copy Markdown
Contributor Author

@claude review again please

@claude

claude Bot commented Jun 19, 2026

Copy link
Copy Markdown

Claude finished @fleupold's task in 2m 26s —— View job


Re-review: Allow unfunded orders with approval

All actionable feedback from the previous round has been addressed:

  • Error swallowing → the allowance call now propagates via map_err(ValidationError::Other)? (commit b91945adc), so a transient RPC failure surfaces as an infra error rather than a misleading InsufficientBalance rejection. ✅
  • Typo spam-protectiion → fixed. ✅
  • Doc grammar in lib.rs → reworded cleanly. ✅

I re-verified the core logic:

  • The new branch in ensure_token_is_transferable only relaxes the non-full_balance_check path and only fires on InsufficientBalance, so InsufficientAllowance / TransferFailed still reject as before. Because can_transfer checks balance before allowance, a zero-balance account reliably hits this branch regardless of its allowance. ✅
  • simulation.rs::allowance mirrors tradable_balance_simple exactly for all three SellTokenSource variants (Erc20 → allowance(owner, vault_relayer); External → hasApprovedRelayer + allowance(owner, vault); Internal → U256::MAX when relayer approved), just dropping the balance component. Correct. ✅
  • ValidationError::Other is consistently mapped to a server-side error elsewhere in this file, so the propagated infra error won't be reported as a user validation failure. ✅
  • The e2e test now proves both paths (reject without approval → InsufficientBalance; accept with a full 2 eth approval → fills once the account is funded with 2 eth WETH). Amounts line up exactly (fee 0), no off-by-one. ✅

Non-blocking observation (no change requested): the approval path requires approval >= sell_amount, which is stricter than the stated spam-protection rationale (any non-zero approval costs equivalent gas to a 1-wei transfer). In practice this is fine — infinite approval is the common case — and erring strict is the safe direction; just noting the asymmetry vs. the 1-atom balance path.

The previously-raised scope question (this also relaxes market orders, not just limit orders) was already acknowledged in the prior thread, so I won't re-open it.

LGTM. 👍
allow-zero-balance-orders-with-approval

@jmg-duarte jmg-duarte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

Comment on lines +2381 to +2383
buy_amount: alloy::primitives::U256::from(1),
sell_amount: alloy::primitives::U256::from(1),
fee_amount: alloy::primitives::U256::from(0),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
buy_amount: alloy::primitives::U256::from(1),
sell_amount: alloy::primitives::U256::from(1),
fee_amount: alloy::primitives::U256::from(0),
buy_amount: U256::ONE,
sell_amount: U256::ONE,
fee_amount: U256::ZERO,

.returning(move |_, _| Err(create_error()));
balance_fetcher
.expect_allowance()
.returning(|_, _, _| Ok(alloy::primitives::U256::ZERO));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
.returning(|_, _, _| Ok(alloy::primitives::U256::ZERO));
.returning(|_, _, _| Ok(U256::ZERO));

.returning(|_, _| Err(TransferSimulationError::InsufficientBalance));
balance_fetcher
.expect_allowance()
.returning(|_, _, _| Ok(alloy::primitives::U256::ZERO));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
.returning(|_, _, _| Ok(alloy::primitives::U256::ZERO));
.returning(|_, _, _| Ok(U256::ZERO));

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.

2 participants