trident_evm_aa/
call_builder.rs1use alloy_network::{Network, TransactionBuilder};
5use alloy_primitives::{Address, Bytes, U256, address};
6use alloy_provider::{PendingTransactionBuilder, Provider, WalletProvider};
7use alloy_sol_types::SolCall;
8
9use crate::abi::{Multicall3, TridentAccount};
10
11const MULTICALL3: Address = address!("cA11bde05977b3631167028862bE2a173976CA11");
13
14#[derive(Debug, thiserror::Error)]
16pub enum MulticallError {
17 #[error("rpc: {0}")]
19 Rpc(Box<dyn std::error::Error + Send + Sync>),
20}
21
22#[derive(Default, Clone)]
25pub struct MulticallBuilder {
26 calls: Vec<Multicall3::Call3Value>,
27 multicall3: Address,
28}
29
30impl MulticallBuilder {
31 pub fn new() -> Self {
33 Self {
34 calls: Vec::new(),
35 multicall3: MULTICALL3,
36 }
37 }
38
39 pub fn with_multicall3(mut self, addr: Address) -> Self {
41 self.multicall3 = addr;
42 self
43 }
44
45 pub fn call<C: SolCall>(mut self, contract: Address, call: &C) -> Self {
47 self.calls.push(Multicall3::Call3Value {
48 target: contract,
49 allowFailure: false,
50 value: U256::ZERO,
51 callData: call.abi_encode().into(),
52 });
53 self
54 }
55
56 pub fn call_with_value<C: SolCall>(mut self, contract: Address, call: &C, value: U256) -> Self {
58 self.calls.push(Multicall3::Call3Value {
59 target: contract,
60 allowFailure: false,
61 value,
62 callData: call.abi_encode().into(),
63 });
64 self
65 }
66
67 pub fn send_value(mut self, to: Address, value: U256) -> Self {
69 self.calls.push(Multicall3::Call3Value {
70 target: to,
71 allowFailure: false,
72 value,
73 callData: Bytes::new(),
74 });
75 self
76 }
77
78 pub fn total_value(&self) -> U256 {
81 self.calls.iter().map(|c| c.value).sum()
82 }
83
84 pub fn aggregate3_calldata(&self) -> Bytes {
86 Multicall3::aggregate3ValueCall {
87 calls: self.calls.clone(),
88 }
89 .abi_encode()
90 .into()
91 }
92
93 pub async fn send_with_wallet_provider<P, N>(
99 self,
100 provider: P,
101 ) -> Result<PendingTransactionBuilder<N>, MulticallError>
102 where
103 P: Provider<N> + WalletProvider<N>,
104 N: Network,
105 {
106 let tx_signer = provider.default_signer_address();
107 self.send(provider, tx_signer).await
108 }
109
110 pub async fn send<P, N>(
120 self,
121 provider: P,
122 tx_signer: Address,
123 ) -> Result<PendingTransactionBuilder<N>, MulticallError>
124 where
125 P: Provider<N>,
126 N: Network,
127 {
128 let tx = self.build_transaction_request::<N>(tx_signer);
129 provider
130 .send_transaction(tx)
131 .await
132 .map_err(|e| MulticallError::Rpc(Box::new(e)))
133 }
134
135 pub fn build_transaction_request<N: Network>(
144 self,
145 tx_signer: Address,
146 ) -> <N as Network>::TransactionRequest {
147 let total = self.total_value();
148 let inner = self.aggregate3_calldata();
149
150 let outer = TridentAccount::executeCall {
151 target: self.multicall3,
152 data: inner,
153 }
154 .abi_encode();
155
156 <N::TransactionRequest as Default>::default()
157 .with_to(tx_signer)
158 .with_value(total)
159 .with_input(outer)
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use alloy_primitives::address;
166
167 use super::*;
168 use crate::{
169 deployer,
170 eip7702_installer::SelfInstaller,
171 test_util::{IWETH9, TestSetup, WETH9_ADDRESS},
172 };
173
174 #[test]
175 fn aggregate3_calldata_starts_with_correct_selector() {
176 let calldata = MulticallBuilder::new()
177 .send_value(Address::ZERO, U256::from(1))
178 .aggregate3_calldata();
179 assert_eq!(&calldata[..4], &Multicall3::aggregate3ValueCall::SELECTOR);
180 }
181
182 #[test]
183 fn total_value_sums_per_call_values() {
184 let builder = MulticallBuilder::new()
185 .call(Address::ZERO, &IWETH9::depositCall {})
186 .call_with_value(Address::ZERO, &IWETH9::depositCall {}, U256::from(5))
187 .send_value(Address::ZERO, U256::from(7));
188 assert_eq!(builder.total_value(), U256::from(12));
189 }
190
191 #[tokio::test]
192 async fn send_executes_multi_step_batch_on_delegated_eoa() {
193 let setup = TestSetup::new().await;
194 let provider = setup.provider();
195 let eoa = setup.signer.address();
196
197 let trident_addr = deployer::deploy(provider.clone())
199 .await
200 .expect("deploy trident");
201 SelfInstaller::new()
202 .with_target(trident_addr)
203 .install(provider.clone(), &setup.signer)
204 .await
205 .expect("install delegation")
206 .get_receipt()
207 .await
208 .expect("install confirmed");
209
210 let bob = address!("00000000000000000000000000000000000000bb");
212 let carol = address!("00000000000000000000000000000000000000cc");
213
214 let deposit_amount = U256::from(500_000_000_000_000_000u128); let transfer_amount = U256::from(100_000_000_000_000_000u128); let send_amount = U256::from(300_000_000_000_000_000u128); let eoa_balance_before = provider.get_balance(eoa).await.expect("balance");
219 let carol_balance_before = provider.get_balance(carol).await.expect("balance");
220
221 MulticallBuilder::new()
223 .call_with_value(WETH9_ADDRESS, &IWETH9::depositCall {}, deposit_amount)
224 .call(
225 WETH9_ADDRESS,
226 &IWETH9::transferCall {
227 to: bob,
228 amount: transfer_amount,
229 },
230 )
231 .send_value(carol, send_amount)
232 .send_with_wallet_provider(provider.clone())
233 .await
234 .expect("multicall send")
235 .get_receipt()
236 .await
237 .expect("multicall confirmed");
238
239 let weth = IWETH9::new(WETH9_ADDRESS, provider.clone());
241 let eoa_weth = weth
242 .balanceOf(eoa)
243 .call()
244 .await
245 .expect("weth balanceOf eoa");
246 let bob_weth = weth
247 .balanceOf(bob)
248 .call()
249 .await
250 .expect("weth balanceOf bob");
251 assert_eq!(eoa_weth, deposit_amount - transfer_amount);
252 assert_eq!(bob_weth, transfer_amount);
253
254 let carol_balance_after = provider.get_balance(carol).await.expect("balance");
256 assert_eq!(
257 carol_balance_after - carol_balance_before,
258 send_amount,
259 "carol should have received exactly send_amount",
260 );
261
262 let eoa_balance_after = provider.get_balance(eoa).await.expect("balance");
263 let eoa_spent = eoa_balance_before - eoa_balance_after;
264 let expected = deposit_amount + send_amount;
268 let gas_ceiling = U256::from(100_000_000_000_000_000u128); assert!(
270 eoa_spent >= expected,
271 "eoa spent ({eoa_spent}) should be at least deposit+send ({expected})"
272 );
273 assert!(
274 eoa_spent <= expected + gas_ceiling,
275 "eoa spent ({eoa_spent}) exceeds deposit+send+gas ceiling — \
276 looks like a double-charge, self-send should be net zero"
277 );
278 }
279}