Skip to content

solana-driver: cut the execution costs from the fills - #5115

Merged
squadgazzz merged 14 commits into
mainfrom
solana-services/be-410-driver-cut
Oct 20, 2026
Merged

squadgazzz merged 14 commits into
mainfrom
solana-services/be-410-driver-cut

Conversation

@squadgazzz

@squadgazzz squadgazzz commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Description

The solver keypair pays each settlement's execution cost and nothing charges it back. With the default fee-handler = "driver" the user gets the engine's fill minus that cost, and the solver keeps the rest.

  1. Before the engine: price the cost (rent of a missing buy token account, network fee) in the limit-leg token at the auction's native prices, move the engine's limit by that cut, then tighten by solver-fee-bps.
  2. After the engine: take the solver fee off the reported fill, then the route's cost, and drop the solution if that breaks the limit.
  3. At settle, the leftover stays in the solver's sell account (buy orders) or the buy-mint buffer (sell orders). The solver keypair pays the rent.

Nothing new per trade: solver_fee stays the volume fee. A solver earns its settlements' buffer deltas, cut plus fee minus route shortfall, swept by BE-374.

Changes

  • fee-handler = "solver" sends the engine the signed limits and the missingBuyTokenAccount flag, solver-fee-bps off
  • Quotes shrink by the solver fee only, the orderbook nets the cost (BE-366)
  • Driver mode skips orders whose limit-leg token has no price

How to test

Updated unit and API tests.

Related issues

BE-410

@linear-code

linear-code Bot commented Oct 16, 2026

Copy link
Copy Markdown

BE-410

@squadgazzz
squadgazzz marked this pull request as ready for review October 19, 2026 11:33
@squadgazzz
squadgazzz requested a review from a team as a code owner October 19, 2026 11:33
@claude

claude Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @squadgazzz's task in 4m 34s —— View job


Review: cut the execution costs from the fills

Thorough read of the diff against origin/solana-services/be-411-quote-network-fee. This is a clean, well-documented change — the new execution_cost module reads clearly, the math (lamports → limit-leg atoms at native prices, div_ceil rounding against the user) checks out, and the tests are genuinely good (boundary cases, saturation, rent-once, per-reason rejection). The Charge enum cleanly unifies the fee-handler split and the mint_info/ata_len refactor is a tidy generalization.

No correctness or safety issues found: every cut is re-validated against the signed limit via respects_limit, so a cut can only ever drop a solution, never under-charge the solver or push a user past their limit.

I posted 2 inline notes, both low-severity "drops otherwise-valid solutions" observations rather than bugs:

  • competition.rs — network_fee_ceiling: the "ceiling" isn't a strict upper bound on the per-route fee apply cuts (different fee sample: cluster-wide vs the solution's writable accounts), so a fill on the signed-limit boundary can be rejected. Safe, but worth confirming it's an accepted trade-off / the naming.
  • execution_cost.rs — rent-on-first-trade: charging the full rent to the first trade can reject aggregate-valid multi-trade fills. Likely moot for the single-trade baseline engine today.

Nothing blocking from my side.
· solana-services/be-410-driver-cut

Comment thread crates/solana-driver/src/domain/competition.rs Outdated
Comment thread crates/solana-driver/src/domain/execution_cost.rs Outdated
Base automatically changed from solana-services/be-411-quote-network-fee to main October 19, 2026 16:45

@tilacog tilacog 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. I just posted a few nit comments.

Comment on lines +127 to +154
let trades = solution
.trades
.iter()
.map(|trade| {
let order = orders
.get(&trade.order_uid)
.copied()
.ok_or(Rejected::UnknownOrder(trade.order_uid))?;
let rent = if rent_charged.insert(order.uid) {
self.rent(order)
} else {
0
};
let lamports = rent + u128::from(fee_share);
let atoms = self
.cut(order, lamports)
.ok_or(Rejected::MissingPrice(order.uid))?;
tracing::debug!(
solution_id,
order = %order.uid,
lamports,
atoms,
"cutting the execution cost from the fill"
);
cut_trade(trade, order, atoms)
})
.collect::<Result<Vec<_>, _>>()?;
solution.trades = trades;

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.

nit: to save a vector allocation for trades, this can also be represented as an open for loop over solution.trades.iter_mut()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If, for example, the third trade fails, iter_mut would have already cut the first two. Collecting first keeps the solution untouched on Err.

Comment on lines +152 to +156
// The idempotent creation tops a pre-funded address up to
// the minimum.
let owed = rent
.minimum_balance(mint.ata_len)
.saturating_sub(snapshot.lamports(&order.buy_token_account));

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.

Nice touch!

Comment on lines +342 to +345
let network_fee = match buy_token_accounts {
Some(_) => self.network_fee_ceiling().await?,
None => 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.

Question: do we use self.network_fee_ceiling() as a placeholder because we can't truly estimate this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes. No route yet, so no accounts and no CU count to price. The fill is cut by its own route's fee afterwards. That cut still prices 1.4M CU, nothing fills cuEstimate before the simulation. Opened BE-446 to track it.

Comment on lines +378 to +379
let mut charged = Vec::with_capacity(solutions.len());
for mut solution in solutions {

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.

Same as my other comment. Since solutions is mut, maybe we just filter/retain it instead of allocating a new vector?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The loop awaits and can bail early, retain_mut is sync. Not worth an index loop for one vector.

@squadgazzz
squadgazzz added this pull request to the merge queue Oct 20, 2026
Merged via the queue into main with commit bdf3aa0 Oct 20, 2026
25 checks passed
@squadgazzz
squadgazzz deleted the solana-services/be-410-driver-cut branch October 20, 2026 07:46
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 20, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants