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, EthereumWallet, 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};
21use foundry_common::shell;
22#[doc(hidden)]
23pub use foundry_config::{Chain, utils::*};
24use foundry_primitives::FoundryTransactionRequest;
25
26sol! {
27 #[sol(rpc)]
28 interface IERC20 {
29 #[derive(Debug)]
30 function name() external view returns (string);
31 function symbol() external view returns (string);
32 function decimals() external view returns (uint8);
33 function totalSupply() external view returns (uint256);
34 function balanceOf(address owner) external view returns (uint256);
35 function transfer(address to, uint256 amount) external returns (bool);
36 function approve(address spender, uint256 amount) external returns (bool);
37 function allowance(address owner, address spender) external view returns (uint256);
38 function mint(address to, uint256 amount) external;
39 function burn(uint256 amount) external;
40 }
41}
42
43#[derive(Debug, Clone, Args)]
47pub struct Erc20TxOpts {
48 #[arg(long, env = "ETH_GAS_LIMIT")]
50 pub gas_limit: Option<U256>,
51
52 #[arg(long, env = "ETH_GAS_PRICE")]
54 pub gas_price: Option<U256>,
55
56 #[arg(long, env = "ETH_PRIORITY_GAS_PRICE")]
58 pub priority_gas_price: Option<U256>,
59
60 #[arg(long)]
62 pub nonce: Option<U64>,
63
64 #[command(flatten)]
65 pub tempo: TempoOpts,
66}
67
68fn apply_tx_opts(
70 tx: &mut WithOtherFields<TransactionRequest>,
71 tx_opts: &Erc20TxOpts,
72 is_legacy: bool,
73) {
74 if let Some(gas_limit) = tx_opts.gas_limit {
75 tx.set_gas_limit(gas_limit.to());
76 }
77
78 if let Some(gas_price) = tx_opts.gas_price {
79 if is_legacy {
80 tx.set_gas_price(gas_price.to());
81 } else {
82 tx.set_max_fee_per_gas(gas_price.to());
83 }
84 }
85
86 if !is_legacy && let Some(priority_fee) = tx_opts.priority_gas_price {
87 tx.set_max_priority_fee_per_gas(priority_fee.to());
88 }
89
90 if let Some(nonce) = tx_opts.nonce {
91 tx.set_nonce(nonce.to());
92 }
93
94 if let Some(fee_token) = tx_opts.tempo.fee_token {
96 tx.other.insert("feeToken".to_string(), serde_json::to_value(fee_token).unwrap());
97 }
98
99 if let Some(nonce_key) = tx_opts.tempo.sequence_key {
100 tx.other.insert("nonceKey".to_string(), serde_json::to_value(nonce_key).unwrap());
101 }
102}
103
104async fn send_erc20_tx<P: Provider<AnyNetwork>>(
108 provider: P,
109 tx: WithOtherFields<TransactionRequest>,
110 send_tx: &SendTxOpts,
111 timeout: u64,
112) -> eyre::Result<()> {
113 if tx.other.contains_key("feeToken") || tx.other.contains_key("nonceKey") {
116 let signer = send_tx.eth.wallet.signer().await?;
117 let mut ftx = FoundryTransactionRequest::new(tx);
118 if ftx.chain_id().is_none() {
119 ftx.set_chain_id(provider.get_chain_id().await?);
120 }
121
122 let signed_tx = ftx.build(&EthereumWallet::new(signer)).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 if shell::is_json() {
368 sh_println!("{}", serde_json::to_string(&allowance.to_string())?)?
369 } else {
370 sh_println!("{}", format_uint_exp(allowance))?
371 }
372 }
373 Self::Balance { token, owner, block, rpc, .. } => {
374 let provider = get_provider_with_curl(&config, rpc.curl)?;
375 let token = token.resolve(&provider).await?;
376 let owner = owner.resolve(&provider).await?;
377
378 let balance = IERC20::new(token, &provider)
379 .balanceOf(owner)
380 .block(block.unwrap_or_default())
381 .call()
382 .await?;
383
384 if shell::is_json() {
385 sh_println!("{}", serde_json::to_string(&balance.to_string())?)?
386 } else {
387 sh_println!("{}", format_uint_exp(balance))?
388 }
389 }
390 Self::Name { token, block, rpc, .. } => {
391 let provider = get_provider_with_curl(&config, rpc.curl)?;
392 let token = token.resolve(&provider).await?;
393
394 let name = IERC20::new(token, &provider)
395 .name()
396 .block(block.unwrap_or_default())
397 .call()
398 .await?;
399
400 if shell::is_json() {
401 sh_println!("{}", serde_json::to_string(&name)?)?
402 } else {
403 sh_println!("{}", name)?
404 }
405 }
406 Self::Symbol { token, block, rpc, .. } => {
407 let provider = get_provider_with_curl(&config, rpc.curl)?;
408 let token = token.resolve(&provider).await?;
409
410 let symbol = IERC20::new(token, &provider)
411 .symbol()
412 .block(block.unwrap_or_default())
413 .call()
414 .await?;
415
416 if shell::is_json() {
417 sh_println!("{}", serde_json::to_string(&symbol)?)?
418 } else {
419 sh_println!("{}", symbol)?
420 }
421 }
422 Self::Decimals { token, block, rpc, .. } => {
423 let provider = get_provider_with_curl(&config, rpc.curl)?;
424 let token = token.resolve(&provider).await?;
425
426 let decimals = IERC20::new(token, &provider)
427 .decimals()
428 .block(block.unwrap_or_default())
429 .call()
430 .await?;
431 sh_println!("{}", decimals)?
432 }
433 Self::TotalSupply { token, block, rpc, .. } => {
434 let provider = get_provider_with_curl(&config, rpc.curl)?;
435 let token = token.resolve(&provider).await?;
436
437 let total_supply = IERC20::new(token, &provider)
438 .totalSupply()
439 .block(block.unwrap_or_default())
440 .call()
441 .await?;
442
443 if shell::is_json() {
444 sh_println!("{}", serde_json::to_string(&total_supply.to_string())?)?
445 } else {
446 sh_println!("{}", format_uint_exp(total_supply))?
447 }
448 }
449 Self::Transfer { token, to, amount, send_tx, tx: tx_opts, .. } => {
451 let provider = signing_provider_with_curl(&send_tx, send_tx.eth.rpc.curl).await?;
452 let mut tx = IERC20::new(token.resolve(&provider).await?, &provider)
453 .transfer(to.resolve(&provider).await?, U256::from_str(&amount)?)
454 .into_transaction_request();
455
456 apply_tx_opts(
458 &mut tx,
459 &tx_opts,
460 get_chain(config.chain, &provider).await?.is_legacy(),
461 );
462
463 send_erc20_tx(
464 provider,
465 tx,
466 &send_tx,
467 send_tx.timeout.unwrap_or(config.transaction_timeout),
468 )
469 .await?
470 }
471 Self::Approve { token, spender, amount, send_tx, tx: tx_opts, .. } => {
472 let provider = signing_provider_with_curl(&send_tx, send_tx.eth.rpc.curl).await?;
473 let mut tx = IERC20::new(token.resolve(&provider).await?, &provider)
474 .approve(spender.resolve(&provider).await?, U256::from_str(&amount)?)
475 .into_transaction_request();
476
477 apply_tx_opts(
479 &mut tx,
480 &tx_opts,
481 get_chain(config.chain, &provider).await?.is_legacy(),
482 );
483
484 send_erc20_tx(
485 provider,
486 tx,
487 &send_tx,
488 send_tx.timeout.unwrap_or(config.transaction_timeout),
489 )
490 .await?
491 }
492 Self::Mint { token, to, amount, send_tx, tx: tx_opts, .. } => {
493 let provider = signing_provider_with_curl(&send_tx, send_tx.eth.rpc.curl).await?;
494 let mut tx = IERC20::new(token.resolve(&provider).await?, &provider)
495 .mint(to.resolve(&provider).await?, U256::from_str(&amount)?)
496 .into_transaction_request();
497
498 apply_tx_opts(
500 &mut tx,
501 &tx_opts,
502 get_chain(config.chain, &provider).await?.is_legacy(),
503 );
504
505 send_erc20_tx(
506 provider,
507 tx,
508 &send_tx,
509 send_tx.timeout.unwrap_or(config.transaction_timeout),
510 )
511 .await?
512 }
513 Self::Burn { token, amount, send_tx, tx: tx_opts, .. } => {
514 let provider = signing_provider_with_curl(&send_tx, send_tx.eth.rpc.curl).await?;
515 let mut tx = IERC20::new(token.resolve(&provider).await?, &provider)
516 .burn(U256::from_str(&amount)?)
517 .into_transaction_request();
518
519 apply_tx_opts(
521 &mut tx,
522 &tx_opts,
523 get_chain(config.chain, &provider).await?.is_legacy(),
524 );
525
526 send_erc20_tx(
527 provider,
528 tx,
529 &send_tx,
530 send_tx.timeout.unwrap_or(config.transaction_timeout),
531 )
532 .await?
533 }
534 };
535 Ok(())
536 }
537}