Skip to main content

odos_sdk/
multicall.rs

1// SPDX-FileCopyrightText: 2025 Semiotic AI, Inc.
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Utilities for batching on-chain queries.
6//!
7//! This module provides two approaches for checking token balances and allowances:
8//!
9//! ## Parallel RPC (Recommended for 2-5 calls)
10//!
11//! Uses `tokio::join_all` to make parallel RPC calls. Simple, no dependencies,
12//! works on any chain.
13//!
14//! ```rust,ignore
15//! use odos_sdk::multicall::{check_balance, check_allowance};
16//!
17//! // Simple parallel checks
18//! let (balance, allowance) = tokio::join!(
19//!     check_balance(&provider, token, owner),
20//!     check_allowance(&provider, token, owner, spender),
21//! );
22//! ```
23//!
24//! ## Multicall3 (Recommended for 10+ calls)
25//!
26//! Batches all calls into a single RPC request. More efficient at scale,
27//! atomic state reads, but requires Multicall3 contract.
28//!
29//! ```rust,ignore
30//! use odos_sdk::multicall::{multicall_check_balances, multicall_check_allowances};
31//!
32//! // Batch many tokens in one RPC call
33//! let tokens = vec![usdc, weth, dai, /* ... many more */];
34//! let balances = multicall_check_balances(&provider, owner, &tokens).await?;
35//! ```
36//!
37//! ## When to Use Which
38//!
39//! | Calls | Recommendation | Reason |
40//! |-------|---------------|--------|
41//! | 1-5   | Parallel RPC  | Simpler, no contract dependency |
42//! | 5-10  | Either        | Similar performance |
43//! | 10+   | Multicall3    | Single RPC, lower latency, less rate limiting |
44
45use alloy_network::{Ethereum, Network};
46use alloy_primitives::{Address, U256};
47use alloy_provider::Provider;
48use alloy_rpc_types::TransactionRequest;
49use alloy_sol_types::{sol, SolCall};
50
51// =============================================================================
52// Simple Parallel RPC Functions (no contract dependency)
53// =============================================================================
54
55/// Checks the ERC20 balance of an address.
56///
57/// Makes a single `eth_call` to the token contract.
58///
59/// # Example
60///
61/// ```rust,ignore
62/// let balance = check_balance(&provider, usdc_address, my_address).await?;
63/// ```
64pub async fn check_balance<P>(
65    provider: &P,
66    token: Address,
67    owner: Address,
68) -> Result<U256, alloy_transport::TransportError>
69where
70    P: Provider<Ethereum>,
71{
72    let calldata = balanceOfCall { owner }.abi_encode();
73    let tx = TransactionRequest::default()
74        .to(token)
75        .input(calldata.into());
76
77    let result = provider.call(tx).await?;
78
79    if result.len() >= 32 {
80        Ok(U256::from_be_slice(&result[..32]))
81    } else {
82        Ok(U256::ZERO)
83    }
84}
85
86/// Checks the ERC20 allowance for a spender.
87///
88/// Makes a single `eth_call` to the token contract.
89///
90/// # Example
91///
92/// ```rust,ignore
93/// let allowance = check_allowance(&provider, usdc_address, my_address, router_address).await?;
94/// ```
95pub async fn check_allowance<P>(
96    provider: &P,
97    token: Address,
98    owner: Address,
99    spender: Address,
100) -> Result<U256, alloy_transport::TransportError>
101where
102    P: Provider<Ethereum>,
103{
104    let calldata = allowanceCall { owner, spender }.abi_encode();
105    let tx = TransactionRequest::default()
106        .to(token)
107        .input(calldata.into());
108
109    let result = provider.call(tx).await?;
110
111    if result.len() >= 32 {
112        Ok(U256::from_be_slice(&result[..32]))
113    } else {
114        Ok(U256::ZERO)
115    }
116}
117
118/// Checks balance and allowance in parallel using tokio.
119///
120/// This is the recommended approach for single-token pre-flight checks.
121///
122/// # Example
123///
124/// ```rust,ignore
125/// let (balance, allowance) = check_balance_and_allowance(
126///     &provider, usdc_address, my_address, router_address
127/// ).await?;
128///
129/// if balance >= amount && allowance >= amount {
130///     println!("Ready to swap!");
131/// }
132/// ```
133pub async fn check_balance_and_allowance<P>(
134    provider: &P,
135    token: Address,
136    owner: Address,
137    spender: Address,
138) -> Result<(U256, U256), alloy_transport::TransportError>
139where
140    P: Provider<Ethereum>,
141{
142    let balance_fut = check_balance(provider, token, owner);
143    let allowance_fut = check_allowance(provider, token, owner, spender);
144
145    let (balance, allowance) = tokio::join!(balance_fut, allowance_fut);
146
147    Ok((balance?, allowance?))
148}
149
150// =============================================================================
151// Multicall3 Functions (for batching many calls)
152// =============================================================================
153
154/// Multicall3 contract deployed at the same address on all major chains.
155pub const MULTICALL3_ADDRESS: Address =
156    alloy_primitives::address!("cA11bde05977b3631167028862bE2a173976CA11");
157
158// ERC20 interface for balance and allowance calls
159sol! {
160    #[allow(missing_docs)]
161    function balanceOf(address owner) external view returns (uint256);
162
163    #[allow(missing_docs)]
164    function allowance(address owner, address spender) external view returns (uint256);
165}
166
167sol! {
168    #[allow(missing_docs)]
169    #[sol(rpc)]
170    interface IMulticall3 {
171        struct Call3 {
172            address target;
173            bool allowFailure;
174            bytes callData;
175        }
176
177        struct Result {
178            bool success;
179            bytes returnData;
180        }
181
182        function aggregate3(Call3[] calldata calls) external payable returns (Result[] memory returnData);
183    }
184}
185
186use IMulticall3::{Call3, IMulticall3Instance, Result as MulticallResult};
187
188/// A pre-flight check for swap prerequisites.
189#[derive(Debug, Clone)]
190pub struct SwapPreflightCheck {
191    /// Token to check.
192    pub token: Address,
193    /// Owner address (who holds the tokens).
194    pub owner: Address,
195    /// Spender address (router that needs approval).
196    pub spender: Address,
197    /// Required amount for the swap.
198    pub required_amount: U256,
199}
200
201/// Result of a pre-flight check.
202#[derive(Debug, Clone)]
203pub struct PreflightResult {
204    /// The token checked.
205    pub token: Address,
206    /// Current balance of the owner.
207    pub balance: U256,
208    /// Current allowance for the spender.
209    pub allowance: U256,
210    /// Whether the balance is sufficient.
211    pub sufficient_balance: bool,
212    /// Whether the allowance is sufficient.
213    pub sufficient_allowance: bool,
214}
215
216impl PreflightResult {
217    /// Returns true if the swap can proceed (sufficient balance and allowance).
218    pub fn is_ready(&self) -> bool {
219        self.sufficient_balance && self.sufficient_allowance
220    }
221
222    /// Returns the amount of additional approval needed, or 0 if sufficient.
223    pub fn approval_needed(&self, required: U256) -> U256 {
224        if self.allowance >= required {
225            U256::ZERO
226        } else {
227            required.saturating_sub(self.allowance)
228        }
229    }
230}
231
232/// Batch check ERC20 balances for multiple tokens using Multicall3.
233///
234/// Fetches all balances in a single RPC call. Recommended for 10+ tokens.
235///
236/// # Arguments
237///
238/// * `provider` - The Alloy provider
239/// * `owner` - Address to check balances for
240/// * `tokens` - List of token addresses to check
241///
242/// # Returns
243///
244/// A vector of balances corresponding to each token in the input list.
245/// Failed calls return `U256::ZERO`.
246///
247/// # Example
248///
249/// ```rust,ignore
250/// let tokens = vec![usdc, weth, dai, link, uni];
251/// let balances = multicall_check_balances(&provider, my_address, &tokens).await?;
252/// ```
253pub async fn multicall_check_balances<N, P>(
254    provider: &P,
255    owner: Address,
256    tokens: &[Address],
257) -> Result<Vec<U256>, alloy_contract::Error>
258where
259    N: Network,
260    P: Provider<N>,
261{
262    if tokens.is_empty() {
263        return Ok(vec![]);
264    }
265
266    let multicall = IMulticall3Instance::new(MULTICALL3_ADDRESS, provider);
267
268    let calls: Vec<Call3> = tokens
269        .iter()
270        .map(|&token| {
271            let calldata = balanceOfCall { owner }.abi_encode();
272            Call3 {
273                target: token,
274                allowFailure: true,
275                callData: calldata.into(),
276            }
277        })
278        .collect();
279
280    let results: Vec<MulticallResult> = multicall.aggregate3(calls).call().await?;
281
282    Ok(results
283        .into_iter()
284        .map(|result| {
285            if result.success && result.returnData.len() >= 32 {
286                U256::from_be_slice(&result.returnData[..32])
287            } else {
288                U256::ZERO
289            }
290        })
291        .collect())
292}
293
294/// Batch check ERC20 allowances for multiple tokens using Multicall3.
295///
296/// Fetches all allowances in a single RPC call. Recommended for 10+ tokens.
297///
298/// # Arguments
299///
300/// * `provider` - The Alloy provider
301/// * `owner` - Address that owns the tokens
302/// * `spender` - Address to check allowance for (e.g., router address)
303/// * `tokens` - List of token addresses to check
304///
305/// # Returns
306///
307/// A vector of allowances corresponding to each token in the input list.
308/// Failed calls return `U256::ZERO`.
309pub async fn multicall_check_allowances<N, P>(
310    provider: &P,
311    owner: Address,
312    spender: Address,
313    tokens: &[Address],
314) -> Result<Vec<U256>, alloy_contract::Error>
315where
316    N: Network,
317    P: Provider<N>,
318{
319    if tokens.is_empty() {
320        return Ok(vec![]);
321    }
322
323    let multicall = IMulticall3Instance::new(MULTICALL3_ADDRESS, provider);
324
325    let calls: Vec<Call3> = tokens
326        .iter()
327        .map(|&token| {
328            let calldata = allowanceCall { owner, spender }.abi_encode();
329            Call3 {
330                target: token,
331                allowFailure: true,
332                callData: calldata.into(),
333            }
334        })
335        .collect();
336
337    let results: Vec<MulticallResult> = multicall.aggregate3(calls).call().await?;
338
339    Ok(results
340        .into_iter()
341        .map(|result| {
342            if result.success && result.returnData.len() >= 32 {
343                U256::from_be_slice(&result.returnData[..32])
344            } else {
345                U256::ZERO
346            }
347        })
348        .collect())
349}
350
351/// Perform pre-flight checks for multiple swaps using Multicall3.
352///
353/// Efficiently checks both balances and allowances in a single batched RPC call,
354/// then returns detailed results for each token. Recommended for 10+ checks.
355///
356/// # Arguments
357///
358/// * `provider` - The Alloy provider
359/// * `checks` - List of pre-flight checks to perform
360///
361/// # Returns
362///
363/// A vector of results indicating whether each swap is ready to execute.
364pub async fn multicall_preflight_checks<N, P>(
365    provider: &P,
366    checks: &[SwapPreflightCheck],
367) -> Result<Vec<PreflightResult>, alloy_contract::Error>
368where
369    N: Network,
370    P: Provider<N>,
371{
372    if checks.is_empty() {
373        return Ok(vec![]);
374    }
375
376    let multicall = IMulticall3Instance::new(MULTICALL3_ADDRESS, provider);
377
378    // Build calls for both balances and allowances
379    let mut calls: Vec<Call3> = Vec::with_capacity(checks.len() * 2);
380
381    for check in checks {
382        // Balance check
383        let balance_calldata = balanceOfCall { owner: check.owner }.abi_encode();
384        calls.push(Call3 {
385            target: check.token,
386            allowFailure: true,
387            callData: balance_calldata.into(),
388        });
389
390        // Allowance check
391        let allowance_calldata = allowanceCall {
392            owner: check.owner,
393            spender: check.spender,
394        }
395        .abi_encode();
396        calls.push(Call3 {
397            target: check.token,
398            allowFailure: true,
399            callData: allowance_calldata.into(),
400        });
401    }
402
403    let results: Vec<MulticallResult> = multicall.aggregate3(calls).call().await?;
404
405    // Parse results in pairs (balance, allowance)
406    Ok(checks
407        .iter()
408        .enumerate()
409        .map(|(i, check)| {
410            let balance_result = &results[i * 2];
411            let allowance_result = &results[i * 2 + 1];
412
413            let balance = if balance_result.success && balance_result.returnData.len() >= 32 {
414                U256::from_be_slice(&balance_result.returnData[..32])
415            } else {
416                U256::ZERO
417            };
418
419            let allowance = if allowance_result.success && allowance_result.returnData.len() >= 32 {
420                U256::from_be_slice(&allowance_result.returnData[..32])
421            } else {
422                U256::ZERO
423            };
424
425            PreflightResult {
426                token: check.token,
427                balance,
428                allowance,
429                sufficient_balance: balance >= check.required_amount,
430                sufficient_allowance: allowance >= check.required_amount,
431            }
432        })
433        .collect())
434}
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439    use alloy_primitives::address;
440
441    #[test]
442    fn test_multicall3_address() {
443        // Multicall3 is deployed at the same address on all major chains
444        assert_eq!(
445            MULTICALL3_ADDRESS,
446            address!("cA11bde05977b3631167028862bE2a173976CA11")
447        );
448    }
449
450    #[test]
451    fn test_preflight_result_is_ready() {
452        let result = PreflightResult {
453            token: Address::ZERO,
454            balance: U256::from(1000),
455            allowance: U256::from(1000),
456            sufficient_balance: true,
457            sufficient_allowance: true,
458        };
459        assert!(result.is_ready());
460
461        let result_insufficient = PreflightResult {
462            token: Address::ZERO,
463            balance: U256::from(1000),
464            allowance: U256::from(100),
465            sufficient_balance: true,
466            sufficient_allowance: false,
467        };
468        assert!(!result_insufficient.is_ready());
469    }
470
471    #[test]
472    fn test_approval_needed() {
473        let result = PreflightResult {
474            token: Address::ZERO,
475            balance: U256::from(1000),
476            allowance: U256::from(500),
477            sufficient_balance: true,
478            sufficient_allowance: false,
479        };
480
481        assert_eq!(result.approval_needed(U256::from(800)), U256::from(300));
482        assert_eq!(result.approval_needed(U256::from(500)), U256::ZERO);
483        assert_eq!(result.approval_needed(U256::from(300)), U256::ZERO);
484    }
485
486    #[test]
487    fn test_balance_of_encoding() {
488        let owner = address!("742d35Cc6634C0532925a3b8D35f3e7a5edD29c0");
489        let calldata = balanceOfCall { owner }.abi_encode();
490
491        // balanceOf selector is 0x70a08231
492        assert_eq!(&calldata[0..4], &[0x70, 0xa0, 0x82, 0x31]);
493    }
494
495    #[test]
496    fn test_allowance_encoding() {
497        let owner = address!("742d35Cc6634C0532925a3b8D35f3e7a5edD29c0");
498        let spender = address!("cf5540fffcdc3d510b18bfca6d2b9987b0772559");
499        let calldata = allowanceCall { owner, spender }.abi_encode();
500
501        // allowance selector is 0xdd62ed3e
502        assert_eq!(&calldata[0..4], &[0xdd, 0x62, 0xed, 0x3e]);
503    }
504}