1use std::str::FromStr;
2
3use crate::{
4 cmd::send::cast_send,
5 format_uint_exp,
6 tx::{CastTxSender, SendTxOpts, signing_provider_with_curl},
7};
8use alloy_eips::{BlockId, Encodable2718};
9use alloy_ens::NameOrAddress;
10use alloy_network::{AnyNetwork, NetworkWallet, TransactionBuilder};
11use alloy_primitives::{U64, U256};
12use alloy_provider::Provider;
13use alloy_rpc_types::TransactionRequest;
14use alloy_serde::WithOtherFields;
15use alloy_sol_types::sol;
16use clap::{Args, Parser};
17use foundry_cli::{
18 opts::{RpcOpts, TempoOpts},
19 utils::{LoadConfig, get_chain, get_provider_with_curl},
20};
21#[doc(hidden)]
22pub use foundry_config::{Chain, utils::*};
23use foundry_primitives::FoundryTransactionRequest;
24
25sol! {
26 #[sol(rpc)]
27 interface IERC20 {
28 #[derive(Debug)]
29 function name() external view returns (string);
30 function symbol() external view returns (string);
31 function decimals() external view returns (uint8);
32 function totalSupply() external view returns (uint256);
33 function balanceOf(address owner) external view returns (uint256);
34 function transfer(address to, uint256 amount) external returns (bool);
35 function approve(address spender, uint256 amount) external returns (bool);
36 function allowance(address owner, address spender) external view returns (uint256);
37 function mint(address to, uint256 amount) external;
38 function burn(uint256 amount) external;
39 }
40}
41
42#[derive(Debug, Clone, Args)]
46pub struct Erc20TxOpts {
47 #[arg(long, env = "ETH_GAS_LIMIT")]
49 pub gas_limit: Option<U256>,
50
51 #[arg(long, env = "ETH_GAS_PRICE")]
53 pub gas_price: Option<U256>,
54
55 #[arg(long, env = "ETH_PRIORITY_GAS_PRICE")]
57 pub priority_gas_price: Option<U256>,
58
59 #[arg(long)]
61 pub nonce: Option<U64>,
62
63 #[command(flatten)]
64 pub tempo: TempoOpts,
65}
66
67fn apply_tx_opts(
69 tx: &mut WithOtherFields<TransactionRequest>,
70 tx_opts: &Erc20TxOpts,
71 is_legacy: bool,
72) {
73 if let Some(gas_limit) = tx_opts.gas_limit {
74 tx.set_gas_limit(gas_limit.to());
75 }
76
77 if let Some(gas_price) = tx_opts.gas_price {
78 if is_legacy {
79 tx.set_gas_price(gas_price.to());
80 } else {
81 tx.set_max_fee_per_gas(gas_price.to());
82 }
83 }
84
85 if !is_legacy && let Some(priority_fee) = tx_opts.priority_gas_price {
86 tx.set_max_priority_fee_per_gas(priority_fee.to());
87 }
88
89 if let Some(nonce) = tx_opts.nonce {
90 tx.set_nonce(nonce.to());
91 }
92
93 if let Some(fee_token) = tx_opts.tempo.fee_token {
95 tx.other.insert("feeToken".to_string(), serde_json::to_value(fee_token).unwrap());
96 }
97
98 if let Some(nonce_key) = tx_opts.tempo.sequence_key {
99 tx.other.insert("nonceKey".to_string(), serde_json::to_value(nonce_key).unwrap());
100 }
101}
102
103async fn send_erc20_tx<P: Provider<AnyNetwork>>(
107 provider: P,
108 tx: WithOtherFields<TransactionRequest>,
109 send_tx: &SendTxOpts,
110 timeout: u64,
111) -> eyre::Result<()> {
112 if tx.other.contains_key("feeToken") || tx.other.contains_key("nonceKey") {
115 let signer = send_tx.eth.wallet.signer().await?;
116 let mut ftx = FoundryTransactionRequest::new(tx);
117 if ftx.chain_id().is_none() {
118 ftx.set_chain_id(provider.get_chain_id().await?);
119 }
120
121 let signed_tx = signer.sign_request(ftx).await?;
123
124 let mut raw_tx = Vec::with_capacity(signed_tx.encode_2718_len());
126 signed_tx.encode_2718(&mut raw_tx);
127
128 let cast = CastTxSender::new(&provider);
129 let pending_tx = cast.send_raw(&raw_tx).await?;
130 let tx_hash = pending_tx.inner().tx_hash();
131
132 if send_tx.cast_async {
133 sh_println!("{tx_hash:#x}")?;
134 } else {
135 let receipt = cast
137 .receipt(format!("{tx_hash:#x}"), None, send_tx.confirmations, Some(timeout), false)
138 .await?;
139 sh_println!("{receipt}")?;
140 }
141
142 return Ok(());
143 }
144
145 cast_send(provider, tx, send_tx.cast_async, send_tx.sync, send_tx.confirmations, timeout).await
147}
148#[derive(Debug, Parser, Clone)]
150pub enum Erc20Subcommand {
151 #[command(visible_alias = "b")]
153 Balance {
154 #[arg(value_parser = NameOrAddress::from_str)]
156 token: NameOrAddress,
157
158 #[arg(value_parser = NameOrAddress::from_str)]
160 owner: NameOrAddress,
161
162 #[arg(long, short = 'B')]
164 block: Option<BlockId>,
165
166 #[command(flatten)]
167 rpc: RpcOpts,
168 },
169
170 #[command(visible_aliases = ["t", "send"])]
172 Transfer {
173 #[arg(value_parser = NameOrAddress::from_str)]
175 token: NameOrAddress,
176
177 #[arg(value_parser = NameOrAddress::from_str)]
179 to: NameOrAddress,
180
181 amount: String,
183
184 #[command(flatten)]
185 send_tx: SendTxOpts,
186
187 #[command(flatten)]
188 tx: Erc20TxOpts,
189 },
190
191 #[command(visible_alias = "a")]
193 Approve {
194 #[arg(value_parser = NameOrAddress::from_str)]
196 token: NameOrAddress,
197
198 #[arg(value_parser = NameOrAddress::from_str)]
200 spender: NameOrAddress,
201
202 amount: String,
204
205 #[command(flatten)]
206 send_tx: SendTxOpts,
207
208 #[command(flatten)]
209 tx: Erc20TxOpts,
210 },
211
212 #[command(visible_alias = "al")]
214 Allowance {
215 #[arg(value_parser = NameOrAddress::from_str)]
217 token: NameOrAddress,
218
219 #[arg(value_parser = NameOrAddress::from_str)]
221 owner: NameOrAddress,
222
223 #[arg(value_parser = NameOrAddress::from_str)]
225 spender: NameOrAddress,
226
227 #[arg(long, short = 'B')]
229 block: Option<BlockId>,
230
231 #[command(flatten)]
232 rpc: RpcOpts,
233 },
234
235 #[command(visible_alias = "n")]
237 Name {
238 #[arg(value_parser = NameOrAddress::from_str)]
240 token: NameOrAddress,
241
242 #[arg(long, short = 'B')]
244 block: Option<BlockId>,
245
246 #[command(flatten)]
247 rpc: RpcOpts,
248 },
249
250 #[command(visible_alias = "s")]
252 Symbol {
253 #[arg(value_parser = NameOrAddress::from_str)]
255 token: NameOrAddress,
256
257 #[arg(long, short = 'B')]
259 block: Option<BlockId>,
260
261 #[command(flatten)]
262 rpc: RpcOpts,
263 },
264
265 #[command(visible_alias = "d")]
267 Decimals {
268 #[arg(value_parser = NameOrAddress::from_str)]
270 token: NameOrAddress,
271
272 #[arg(long, short = 'B')]
274 block: Option<BlockId>,
275
276 #[command(flatten)]
277 rpc: RpcOpts,
278 },
279
280 #[command(visible_alias = "ts")]
282 TotalSupply {
283 #[arg(value_parser = NameOrAddress::from_str)]
285 token: NameOrAddress,
286
287 #[arg(long, short = 'B')]
289 block: Option<BlockId>,
290
291 #[command(flatten)]
292 rpc: RpcOpts,
293 },
294
295 #[command(visible_alias = "m")]
297 Mint {
298 #[arg(value_parser = NameOrAddress::from_str)]
300 token: NameOrAddress,
301
302 #[arg(value_parser = NameOrAddress::from_str)]
304 to: NameOrAddress,
305
306 amount: String,
308
309 #[command(flatten)]
310 send_tx: SendTxOpts,
311
312 #[command(flatten)]
313 tx: Erc20TxOpts,
314 },
315
316 #[command(visible_alias = "bu")]
318 Burn {
319 #[arg(value_parser = NameOrAddress::from_str)]
321 token: NameOrAddress,
322
323 amount: String,
325
326 #[command(flatten)]
327 send_tx: SendTxOpts,
328
329 #[command(flatten)]
330 tx: Erc20TxOpts,
331 },
332}
333
334impl Erc20Subcommand {
335 fn rpc(&self) -> &RpcOpts {
336 match self {
337 Self::Allowance { rpc, .. } => rpc,
338 Self::Approve { send_tx, .. } => &send_tx.eth.rpc,
339 Self::Balance { rpc, .. } => rpc,
340 Self::Transfer { send_tx, .. } => &send_tx.eth.rpc,
341 Self::Name { rpc, .. } => rpc,
342 Self::Symbol { rpc, .. } => rpc,
343 Self::Decimals { rpc, .. } => rpc,
344 Self::TotalSupply { rpc, .. } => rpc,
345 Self::Mint { send_tx, .. } => &send_tx.eth.rpc,
346 Self::Burn { send_tx, .. } => &send_tx.eth.rpc,
347 }
348 }
349
350 pub async fn run(self) -> eyre::Result<()> {
351 let config = self.rpc().load_config()?;
352
353 match self {
354 Self::Allowance { token, owner, spender, block, rpc, .. } => {
356 let provider = get_provider_with_curl(&config, rpc.curl)?;
357 let token = token.resolve(&provider).await?;
358 let owner = owner.resolve(&provider).await?;
359 let spender = spender.resolve(&provider).await?;
360
361 let allowance = IERC20::new(token, &provider)
362 .allowance(owner, spender)
363 .block(block.unwrap_or_default())
364 .call()
365 .await?;
366
367 sh_println!("{}", format_uint_exp(allowance))?
368 }
369 Self::Balance { token, owner, block, rpc, .. } => {
370 let provider = get_provider_with_curl(&config, rpc.curl)?;
371 let token = token.resolve(&provider).await?;
372 let owner = owner.resolve(&provider).await?;
373
374 let balance = IERC20::new(token, &provider)
375 .balanceOf(owner)
376 .block(block.unwrap_or_default())
377 .call()
378 .await?;
379 sh_println!("{}", format_uint_exp(balance))?
380 }
381 Self::Name { token, block, rpc, .. } => {
382 let provider = get_provider_with_curl(&config, rpc.curl)?;
383 let token = token.resolve(&provider).await?;
384
385 let name = IERC20::new(token, &provider)
386 .name()
387 .block(block.unwrap_or_default())
388 .call()
389 .await?;
390 sh_println!("{}", name)?
391 }
392 Self::Symbol { token, block, rpc, .. } => {
393 let provider = get_provider_with_curl(&config, rpc.curl)?;
394 let token = token.resolve(&provider).await?;
395
396 let symbol = IERC20::new(token, &provider)
397 .symbol()
398 .block(block.unwrap_or_default())
399 .call()
400 .await?;
401 sh_println!("{}", symbol)?
402 }
403 Self::Decimals { token, block, rpc, .. } => {
404 let provider = get_provider_with_curl(&config, rpc.curl)?;
405 let token = token.resolve(&provider).await?;
406
407 let decimals = IERC20::new(token, &provider)
408 .decimals()
409 .block(block.unwrap_or_default())
410 .call()
411 .await?;
412 sh_println!("{}", decimals)?
413 }
414 Self::TotalSupply { token, block, rpc, .. } => {
415 let provider = get_provider_with_curl(&config, rpc.curl)?;
416 let token = token.resolve(&provider).await?;
417
418 let total_supply = IERC20::new(token, &provider)
419 .totalSupply()
420 .block(block.unwrap_or_default())
421 .call()
422 .await?;
423 sh_println!("{}", format_uint_exp(total_supply))?
424 }
425 Self::Transfer { token, to, amount, send_tx, tx: tx_opts, .. } => {
427 let provider = signing_provider_with_curl(&send_tx, send_tx.eth.rpc.curl).await?;
428 let mut tx = IERC20::new(token.resolve(&provider).await?, &provider)
429 .transfer(to.resolve(&provider).await?, U256::from_str(&amount)?)
430 .into_transaction_request();
431
432 apply_tx_opts(
434 &mut tx,
435 &tx_opts,
436 get_chain(config.chain, &provider).await?.is_legacy(),
437 );
438
439 send_erc20_tx(
440 provider,
441 tx,
442 &send_tx,
443 send_tx.timeout.unwrap_or(config.transaction_timeout),
444 )
445 .await?
446 }
447 Self::Approve { token, spender, amount, send_tx, tx: tx_opts, .. } => {
448 let provider = signing_provider_with_curl(&send_tx, send_tx.eth.rpc.curl).await?;
449 let mut tx = IERC20::new(token.resolve(&provider).await?, &provider)
450 .approve(spender.resolve(&provider).await?, U256::from_str(&amount)?)
451 .into_transaction_request();
452
453 apply_tx_opts(
455 &mut tx,
456 &tx_opts,
457 get_chain(config.chain, &provider).await?.is_legacy(),
458 );
459
460 send_erc20_tx(
461 provider,
462 tx,
463 &send_tx,
464 send_tx.timeout.unwrap_or(config.transaction_timeout),
465 )
466 .await?
467 }
468 Self::Mint { token, to, amount, send_tx, tx: tx_opts, .. } => {
469 let provider = signing_provider_with_curl(&send_tx, send_tx.eth.rpc.curl).await?;
470 let mut tx = IERC20::new(token.resolve(&provider).await?, &provider)
471 .mint(to.resolve(&provider).await?, U256::from_str(&amount)?)
472 .into_transaction_request();
473
474 apply_tx_opts(
476 &mut tx,
477 &tx_opts,
478 get_chain(config.chain, &provider).await?.is_legacy(),
479 );
480
481 send_erc20_tx(
482 provider,
483 tx,
484 &send_tx,
485 send_tx.timeout.unwrap_or(config.transaction_timeout),
486 )
487 .await?
488 }
489 Self::Burn { token, amount, send_tx, tx: tx_opts, .. } => {
490 let provider = signing_provider_with_curl(&send_tx, send_tx.eth.rpc.curl).await?;
491 let mut tx = IERC20::new(token.resolve(&provider).await?, &provider)
492 .burn(U256::from_str(&amount)?)
493 .into_transaction_request();
494
495 apply_tx_opts(
497 &mut tx,
498 &tx_opts,
499 get_chain(config.chain, &provider).await?.is_legacy(),
500 );
501
502 send_erc20_tx(
503 provider,
504 tx,
505 &send_tx,
506 send_tx.timeout.unwrap_or(config.transaction_timeout),
507 )
508 .await?
509 }
510 };
511 Ok(())
512 }
513}