1use std::str::FromStr;
2
3use crate::{
4 cmd::send::cast_send,
5 format_uint_exp,
6 tx::{CastTxSender, SendTxOpts, get_provider_with_wallet},
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},
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, .. } => {
356 let provider = get_provider(&config)?;
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, .. } => {
374 let provider = get_provider(&config)?;
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, .. } => {
391 let provider = get_provider(&config)?;
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, .. } => {
407 let provider = get_provider(&config)?;
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, .. } => {
423 let provider = get_provider(&config)?;
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 if shell::is_json() {
432 sh_println!("{}", serde_json::to_string(&decimals)?)?
433 } else {
434 sh_println!("{}", decimals)?
435 }
436 }
437 Self::TotalSupply { token, block, .. } => {
438 let provider = get_provider(&config)?;
439 let token = token.resolve(&provider).await?;
440
441 let total_supply = IERC20::new(token, &provider)
442 .totalSupply()
443 .block(block.unwrap_or_default())
444 .call()
445 .await?;
446
447 if shell::is_json() {
448 sh_println!("{}", serde_json::to_string(&total_supply.to_string())?)?
449 } else {
450 sh_println!("{}", format_uint_exp(total_supply))?
451 }
452 }
453 Self::Transfer { token, to, amount, send_tx, tx: tx_opts, .. } => {
455 let provider = get_provider_with_wallet(&send_tx).await?;
456 let mut tx = IERC20::new(token.resolve(&provider).await?, &provider)
457 .transfer(to.resolve(&provider).await?, U256::from_str(&amount)?)
458 .into_transaction_request();
459
460 apply_tx_opts(
462 &mut tx,
463 &tx_opts,
464 get_chain(config.chain, &provider).await?.is_legacy(),
465 );
466
467 send_erc20_tx(
468 provider,
469 tx,
470 &send_tx,
471 send_tx.timeout.unwrap_or(config.transaction_timeout),
472 )
473 .await?
474 }
475 Self::Approve { token, spender, amount, send_tx, tx: tx_opts, .. } => {
476 let provider = get_provider_with_wallet(&send_tx).await?;
477 let mut tx = IERC20::new(token.resolve(&provider).await?, &provider)
478 .approve(spender.resolve(&provider).await?, U256::from_str(&amount)?)
479 .into_transaction_request();
480
481 apply_tx_opts(
483 &mut tx,
484 &tx_opts,
485 get_chain(config.chain, &provider).await?.is_legacy(),
486 );
487
488 send_erc20_tx(
489 provider,
490 tx,
491 &send_tx,
492 send_tx.timeout.unwrap_or(config.transaction_timeout),
493 )
494 .await?
495 }
496 Self::Mint { token, to, amount, send_tx, tx: tx_opts, .. } => {
497 let provider = get_provider_with_wallet(&send_tx).await?;
498 let mut tx = IERC20::new(token.resolve(&provider).await?, &provider)
499 .mint(to.resolve(&provider).await?, U256::from_str(&amount)?)
500 .into_transaction_request();
501
502 apply_tx_opts(
504 &mut tx,
505 &tx_opts,
506 get_chain(config.chain, &provider).await?.is_legacy(),
507 );
508
509 send_erc20_tx(
510 provider,
511 tx,
512 &send_tx,
513 send_tx.timeout.unwrap_or(config.transaction_timeout),
514 )
515 .await?
516 }
517 Self::Burn { token, amount, send_tx, tx: tx_opts, .. } => {
518 let provider = get_provider_with_wallet(&send_tx).await?;
519 let mut tx = IERC20::new(token.resolve(&provider).await?, &provider)
520 .burn(U256::from_str(&amount)?)
521 .into_transaction_request();
522
523 apply_tx_opts(
525 &mut tx,
526 &tx_opts,
527 get_chain(config.chain, &provider).await?.is_legacy(),
528 );
529
530 send_erc20_tx(
531 provider,
532 tx,
533 &send_tx,
534 send_tx.timeout.unwrap_or(config.transaction_timeout),
535 )
536 .await?
537 }
538 };
539 Ok(())
540 }
541}