55//! [`strategy::Swap`] which holds simulated/expected values computed before
66//! execution. Used by the execution worker to compute `RealizedProfit`.
77//!
8- //! Native ETH inputs/outputs do not emit Transfer events — for those legs the
9- //! expected amounts from the simulation are used as a fallback (see TODO in
10- //! `try_from_receipts`).
8+ //! Native ETH inputs/outputs do not emit Transfer events. For those legs the
9+ //! actual amount is derived from the signer's balance diff across the receipt
10+ //! block: `pre_balance − post_balance − gas_cost` for ETH in, and
11+ //! `post_balance − pre_balance + gas_cost` for ETH out.
1112
1213use crate :: {
1314 state:: { erc20:: Transfer , pair:: Pair } ,
1415 strategy,
1516} ;
16- use alloy:: { primitives:: Address , rpc:: types:: TransactionReceipt } ;
17+ use alloy:: {
18+ primitives:: Address ,
19+ providers:: Provider ,
20+ rpc:: types:: TransactionReceipt ,
21+ } ;
1722use color_eyre:: eyre:: { self } ;
1823use num_bigint:: BigUint ;
19- use num_traits:: CheckedAdd as _;
24+ use num_traits:: { CheckedAdd as _, CheckedSub as _ } ;
2025use serde:: { Deserialize , Serialize } ;
2126use tycho_simulation:: tycho_common:: models:: token:: Token ;
2227
@@ -29,11 +34,11 @@ use tycho_simulation::tycho_common::models::token::Token;
2934pub struct Swap {
3035 /// The token that was sold in the swap.
3136 pub token_in : Token ,
32- /// Actual amount of token_in transferred (from Transfer events).
37+ /// Actual amount of token_in transferred (from Transfer events or balance diff ).
3338 pub amount_in : BigUint ,
3439 /// The token that was received in the swap.
3540 pub token_out : Token ,
36- /// Actual amount of token_out received (from Transfer events).
41+ /// Actual amount of token_out received (from Transfer events or balance diff ).
3742 pub amount_out : BigUint ,
3843 /// Gas cost in wei (gas_used * effective_gas_price from receipt).
3944 pub gas_cost_eth : BigUint ,
@@ -42,31 +47,66 @@ pub struct Swap {
4247impl Swap {
4348 /// Parse actual swap amounts from a transaction receipt.
4449 ///
45- /// Iterates through the transaction logs to find ERC20 Transfer events for
46- /// the input and output tokens, summing the transferred amounts. Also extracts
47- /// the actual gas cost from the receipt.
48- pub fn try_from_receipts (
50+ /// For ERC20 tokens, iterates through the transaction logs to find Transfer
51+ /// events, summing the transferred amounts. For native ETH legs, queries the
52+ /// signer's balance at block N-1 and block N to derive the actual amount from
53+ /// the on-chain balance diff (assumes this is the only tx from the signer in
54+ /// that block, which holds for kuma's serial execution model).
55+ pub async fn try_from_receipts < P : Provider > (
4956 receipt : & TransactionReceipt ,
5057 swap : strategy:: Swap ,
58+ provider : & P ,
5159 ) -> eyre:: Result < Self > {
5260 let mut amount_in = BigUint :: default ( ) ;
5361 let mut amount_out = BigUint :: default ( ) ;
5462
5563 let token_in_addr = Address :: from_slice ( & swap. token_in . address ) ;
5664 let token_out_addr = Address :: from_slice ( & swap. token_out . address ) ;
5765
58- // Handle native ETH input (doesn't emit Transfer events)
59- if token_in_addr == Address :: ZERO {
60- // TODO: For ETH input, we should get the actual transaction value from receipt
61- // For now, use the expected amount from the swap simulation
62- amount_in = swap. amount_in . clone ( ) ;
63- }
66+ let gas_units = BigUint :: from ( receipt. gas_used ) ;
67+ let wei_per_gas = BigUint :: from ( receipt. effective_gas_price ) ;
68+ let gas_cost_eth = gas_units * wei_per_gas;
69+
70+ if token_in_addr == Address :: ZERO || token_out_addr == Address :: ZERO {
71+ let from = receipt. from ;
72+ let block_number = receipt
73+ . block_number
74+ . ok_or_else ( || eyre:: eyre!( "receipt missing block number" ) ) ?;
75+ let pre_block = block_number
76+ . checked_sub ( 1 )
77+ . ok_or_else ( || eyre:: eyre!( "block number is 0, cannot query pre-block balance" ) ) ?;
78+
79+ let pre_balance = provider
80+ . get_balance ( from)
81+ . block_id ( pre_block. into ( ) )
82+ . await
83+ . map_err ( |e| eyre:: eyre!( "failed to get pre-block balance: {e}" ) ) ?;
84+ let post_balance = provider
85+ . get_balance ( from)
86+ . block_id ( block_number. into ( ) )
87+ . await
88+ . map_err ( |e| eyre:: eyre!( "failed to get post-block balance: {e}" ) ) ?;
89+
90+ let pre = BigUint :: from_bytes_be ( & pre_balance. to_be_bytes :: < 32 > ( ) ) ;
91+ let post = BigUint :: from_bytes_be ( & post_balance. to_be_bytes :: < 32 > ( ) ) ;
6492
65- // Handle native ETH output (doesn't emit Transfer events)
66- if token_out_addr == Address :: ZERO {
67- // For ETH output, we need to calculate from balance changes
68- // This is more complex - for now use the expected amount from swap
69- amount_out = swap. amount_out . clone ( ) ;
93+ if token_in_addr == Address :: ZERO {
94+ // Signer spent amount_in + gas_cost, so balance dropped by that amount.
95+ amount_in = pre
96+ . checked_sub ( & post)
97+ . ok_or_else ( || eyre:: eyre!( "ETH balance increased unexpectedly on ETH-in swap" ) ) ?
98+ . checked_sub ( & gas_cost_eth)
99+ . ok_or_else ( || eyre:: eyre!( "balance diff smaller than gas cost on ETH-in swap" ) ) ?;
100+ }
101+
102+ if token_out_addr == Address :: ZERO {
103+ // Signer received amount_out and paid gas_cost, net change is amount_out - gas_cost.
104+ amount_out = post
105+ . checked_sub ( & pre)
106+ . ok_or_else ( || eyre:: eyre!( "ETH balance decreased unexpectedly on ETH-out swap" ) ) ?
107+ . checked_add ( & gas_cost_eth)
108+ . ok_or_else ( || eyre:: eyre!( "overflow computing ETH amount_out" ) ) ?;
109+ }
70110 }
71111
72112 for log in receipt. logs ( ) {
@@ -85,11 +125,6 @@ impl Swap {
85125 }
86126 }
87127
88- let gas_units = BigUint :: from ( receipt. gas_used ) ;
89- let wei_per_gas = BigUint :: from ( receipt. effective_gas_price ) ;
90- // cost in wei, e.g. 5 × 10^14 wei = 500,000 Gwei ~ 0.0005 ETH
91- let gas_cost_eth = gas_units * wei_per_gas;
92-
93128 Ok ( Swap {
94129 token_in : swap. token_in ,
95130 amount_in,
0 commit comments