alloy_contract/
multicall.rs

1//! Multicall module to organize and implement required functionality for enabling multicall in
2//! alloy_provider plus related tests. This avoids cyclic deps between alloy_provider and
3//! alloy_contract.
4//!
5//! This module is not public API.
6use super::SolCallBuilder;
7use alloy_network::{Network, TransactionBuilder};
8use alloy_primitives::{Address, Bytes};
9use alloy_provider::{MulticallItem, Provider};
10use alloy_sol_types::SolCall;
11
12impl<T, P: Provider<N>, C: SolCall, N: Network> MulticallItem for SolCallBuilder<T, P, C, N> {
13    type Decoder = C;
14
15    fn target(&self) -> Address {
16        self.request.to().expect("`to` not set for the `SolCallBuilder`")
17    }
18
19    fn input(&self) -> Bytes {
20        self.calldata().clone()
21    }
22}
23
24#[cfg(test)]
25mod tests {
26    use super::*;
27    use alloy_primitives::{address, b256, U256};
28    use alloy_provider::{
29        CallItem, CallItemBuilder, Failure, MulticallBuilder, Provider, ProviderBuilder,
30    };
31    use alloy_sol_types::sol;
32    use DummyThatFails::DummyThatFailsInstance;
33
34    sol! {
35        #[derive(Debug, PartialEq)]
36        #[sol(rpc)]
37        interface ERC20 {
38            function totalSupply() external view returns (uint256 totalSupply);
39            function balanceOf(address owner) external view returns (uint256 balance);
40            function transfer(address to, uint256 value) external returns (bool);
41        }
42    }
43
44    sol! {
45        // solc 0.8.25; solc DummyThatFails.sol --optimize --bin
46        #[sol(rpc, bytecode = "6080604052348015600e575f80fd5b5060a780601a5f395ff3fe6080604052348015600e575f80fd5b50600436106030575f3560e01c80630b93381b146034578063a9cc4718146036575b5f80fd5b005b603460405162461bcd60e51b815260040160689060208082526004908201526319985a5b60e21b604082015260600190565b60405180910390fdfea2646970667358221220c90ee107375422bb3516f4f13cdd754387c374edb5d9815fb6aa5ca111a77cb264736f6c63430008190033")]
47        #[derive(Debug)]
48        contract DummyThatFails {
49            function fail() external {
50                revert("fail");
51            }
52
53            function success() external {}
54        }
55    }
56
57    async fn deploy_dummy(
58        provider: impl alloy_provider::Provider,
59    ) -> DummyThatFailsInstance<(), impl alloy_provider::Provider> {
60        DummyThatFails::deploy(provider).await.unwrap()
61    }
62
63    const FORK_URL: &str = "https://reth-ethereum.ithaca.xyz/rpc";
64
65    #[tokio::test]
66    async fn test_single() {
67        let weth = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
68        let provider = ProviderBuilder::new().on_anvil_with_config(|a| a.fork(FORK_URL));
69
70        let erc20 = ERC20::new(weth, &provider);
71        let multicall = provider.multicall().add(erc20.totalSupply());
72
73        let (_total_supply,) = multicall.aggregate().await.unwrap();
74    }
75
76    #[tokio::test]
77    async fn test_aggregate() {
78        let weth = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
79        let provider = ProviderBuilder::new().on_anvil_with_config(|a| a.fork(FORK_URL));
80
81        let erc20 = ERC20::new(weth, &provider);
82
83        let multicall = provider
84            .multicall()
85            .add(erc20.totalSupply())
86            .add(erc20.balanceOf(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")))
87            .add(erc20.totalSupply())
88            .add(erc20.balanceOf(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")));
89
90        let (t1, b1, t2, b2) = multicall.aggregate().await.unwrap();
91
92        assert_eq!(t1, t2);
93        assert_eq!(b1, b2);
94    }
95
96    #[tokio::test]
97    async fn test_try_aggregate_pass() {
98        let weth = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
99        let provider = ProviderBuilder::new().on_anvil_with_config(|a| a.fork(FORK_URL));
100        let erc20 = ERC20::new(weth, &provider);
101
102        let multicall = provider
103            .multicall()
104            .add(erc20.totalSupply())
105            .add(erc20.balanceOf(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")))
106            .add(erc20.totalSupply())
107            .add(erc20.balanceOf(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")));
108
109        let (_t1, _b1, _t2, _b2) = multicall.try_aggregate(true).await.unwrap();
110    }
111
112    #[tokio::test]
113    async fn aggregate3() {
114        let weth = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
115
116        let provider =
117            ProviderBuilder::new().on_anvil_with_wallet_and_config(|a| a.fork(FORK_URL)).unwrap();
118
119        let dummy = deploy_dummy(provider.clone()).await;
120        let erc20 = ERC20::new(weth, &provider);
121        let multicall = provider
122            .multicall()
123            .add(erc20.totalSupply())
124            .add(erc20.balanceOf(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")))
125            .add(dummy.fail()); // Failing call that will revert the multicall.
126
127        let err = multicall.aggregate3().await.unwrap_err();
128
129        assert!(err.to_string().contains("revert: Multicall3: call failed"));
130
131        let failing_call = CallItemBuilder::new(dummy.fail()).allow_failure(true);
132        let multicall = provider
133            .multicall()
134            .add(erc20.totalSupply())
135            .add(erc20.balanceOf(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")))
136            .add_call(failing_call);
137        let (t1, b1, failure) = multicall.aggregate3().await.unwrap();
138
139        assert!(t1.is_ok());
140        assert!(b1.is_ok());
141        let err = failure.unwrap_err();
142        assert!(matches!(err, Failure { idx: 2, return_data: _ }));
143    }
144
145    #[tokio::test]
146    async fn test_try_aggregate_fail() {
147        let weth = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
148        let provider =
149            ProviderBuilder::new().on_anvil_with_wallet_and_config(|a| a.fork(FORK_URL)).unwrap();
150
151        let dummy_addr = deploy_dummy(provider.clone()).await;
152        let erc20 = ERC20::new(weth, &provider);
153        let multicall = provider
154            .multicall()
155            .add(erc20.totalSupply())
156            .add(erc20.balanceOf(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")))
157            .add(erc20.totalSupply())
158            .add(erc20.balanceOf(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")))
159            .add(dummy_addr.fail());
160
161        let err = multicall.try_aggregate(true).await.unwrap_err();
162
163        assert!(err.to_string().contains("revert: Multicall3: call failed"));
164
165        let (t1, b1, t2, b2, failure) = multicall.try_aggregate(false).await.unwrap();
166
167        assert!(t1.is_ok());
168        assert!(b1.is_ok());
169        assert!(t2.is_ok());
170        assert!(b2.is_ok());
171        let err = failure.unwrap_err();
172        assert!(matches!(err, Failure { idx: 4, return_data: _ }));
173    }
174
175    #[tokio::test]
176    async fn test_util() {
177        let weth = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
178        let provider = ProviderBuilder::new()
179            .on_anvil_with_config(|a| a.fork(FORK_URL).fork_block_number(21787144));
180        let erc20 = ERC20::new(weth, &provider);
181        let multicall = provider
182            .multicall()
183            .add(erc20.totalSupply())
184            .add(erc20.balanceOf(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")))
185            .add(erc20.totalSupply())
186            .add(erc20.balanceOf(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")))
187            .get_block_hash(21787144);
188
189        let (t1, b1, t2, b2, block_hash) = multicall.aggregate().await.unwrap();
190
191        assert_eq!(t1, t2);
192        assert_eq!(b1, b2);
193        assert_eq!(
194            block_hash.blockHash,
195            b256!("31be03d4fb9a280d1699f1004f340573cd6d717dae79095d382e876415cb26ba")
196        );
197    }
198
199    sol! {
200        // solc 0.8.25; solc PayableCounter.sol --optimize --bin
201        #[sol(rpc, bytecode = "6080604052348015600e575f80fd5b5061012c8061001c5f395ff3fe6080604052600436106025575f3560e01c806361bc221a146029578063d09de08a14604d575b5f80fd5b3480156033575f80fd5b50603b5f5481565b60405190815260200160405180910390f35b60536055565b005b5f341160bc5760405162461bcd60e51b815260206004820152602c60248201527f50617961626c65436f756e7465723a2076616c7565206d75737420626520677260448201526b06561746572207468616e20360a41b606482015260840160405180910390fd5b60015f8082825460cb919060d2565b9091555050565b8082018082111560f057634e487b7160e01b5f52601160045260245ffd5b9291505056fea264697066735822122064d656316647d3dc48d7ef0466bd10bc87694802a673183058725926a5190a5564736f6c63430008190033")]
202        #[derive(Debug)]
203        contract PayableCounter {
204            uint256 public counter;
205
206            function increment() public payable {
207                require(msg.value > 0, "PayableCounter: value must be greater than 0");
208                counter += 1;
209            }
210        }
211    }
212
213    #[tokio::test]
214    async fn aggregate3_value() {
215        let provider =
216            ProviderBuilder::new().on_anvil_with_wallet_and_config(|a| a.fork(FORK_URL)).unwrap();
217
218        let payable_counter = PayableCounter::deploy(provider.clone()).await.unwrap();
219
220        let increment_call = CallItem::<PayableCounter::incrementCall>::new(
221            payable_counter.increment().target(),
222            payable_counter.increment().input(),
223        )
224        .value(U256::from(100));
225
226        let multicall = provider
227            .multicall()
228            .add(payable_counter.counter())
229            .add_call(increment_call)
230            .add(payable_counter.counter());
231
232        let (c1, inc, c2) = multicall.aggregate3_value().await.unwrap();
233
234        assert_eq!(c1.unwrap().counter, U256::ZERO);
235        assert!(inc.is_ok());
236        assert_eq!(c2.unwrap().counter, U256::from(1));
237
238        // Allow failure - due to no value being sent
239        let increment_call = CallItem::<PayableCounter::incrementCall>::new(
240            payable_counter.increment().target(),
241            payable_counter.increment().input(),
242        )
243        .allow_failure(true);
244
245        let multicall = provider
246            .multicall()
247            .add(payable_counter.counter())
248            .add_call(increment_call)
249            .add(payable_counter.counter());
250
251        let (c1, inc, c2) = multicall.aggregate3_value().await.unwrap();
252
253        assert_eq!(c1.unwrap().counter, U256::ZERO);
254        assert!(inc.is_err_and(|failure| matches!(failure, Failure { idx: 1, return_data: _ })));
255        assert_eq!(c2.unwrap().counter, U256::ZERO);
256    }
257
258    #[tokio::test]
259    async fn test_clear() {
260        let weth = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
261        let provider = ProviderBuilder::new().on_anvil();
262
263        let erc20 = ERC20::new(weth, &provider);
264        let multicall = provider
265            .multicall()
266            .add(erc20.totalSupply())
267            .add(erc20.balanceOf(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")));
268        assert_eq!(multicall.len(), 2);
269        let multicall = multicall.clear();
270        assert_eq!(multicall.len(), 0);
271    }
272
273    #[tokio::test]
274    async fn add_dynamic() {
275        let weth = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
276        let provider = ProviderBuilder::new().on_anvil_with_config(|a| a.fork(FORK_URL));
277
278        let erc20 = ERC20::new(weth, &provider);
279
280        let multicall = MulticallBuilder::new_dynamic(provider.clone())
281            .add_dynamic(erc20.totalSupply())
282            // .add(erc20.balanceOf(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"))) - WON'T
283            // COMPILE
284            // .add_dynamic(erc20.balanceOf(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"))) -
285            // WON'T COMPILE
286            .add_dynamic(erc20.totalSupply())
287            .extend(vec![erc20.totalSupply(), erc20.totalSupply()]);
288
289        let res: Vec<ERC20::totalSupplyReturn> = multicall.aggregate().await.unwrap();
290
291        assert_eq!(res.len(), 4);
292        assert_eq!(res[0], res[1]);
293    }
294
295    #[tokio::test]
296    async fn test_extend_dynamic() {
297        let weth = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
298        let provider = ProviderBuilder::new().on_anvil_with_config(|a| a.fork(FORK_URL));
299        let erc20 = ERC20::new(weth, &provider);
300        let ts_calls = vec![erc20.totalSupply(); 18];
301        let multicall = MulticallBuilder::new_dynamic(provider.clone()).extend(ts_calls);
302
303        assert_eq!(multicall.len(), 18);
304        let res = multicall.aggregate().await.unwrap();
305        assert_eq!(res.len(), 18);
306    }
307}