Skip to main content

cast/cmd/
erc20.rs

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/// Transaction options for ERC20 operations.
44///
45/// This struct contains only the transaction options relevant to ERC20 token interactions
46#[derive(Debug, Clone, Args)]
47pub struct Erc20TxOpts {
48    /// Gas limit for the transaction.
49    #[arg(long, env = "ETH_GAS_LIMIT")]
50    pub gas_limit: Option<U256>,
51
52    /// Gas price for legacy transactions, or max fee per gas for EIP1559 transactions.
53    #[arg(long, env = "ETH_GAS_PRICE")]
54    pub gas_price: Option<U256>,
55
56    /// Max priority fee per gas for EIP1559 transactions.
57    #[arg(long, env = "ETH_PRIORITY_GAS_PRICE")]
58    pub priority_gas_price: Option<U256>,
59
60    /// Nonce for the transaction.
61    #[arg(long)]
62    pub nonce: Option<U64>,
63
64    #[command(flatten)]
65    pub tempo: TempoOpts,
66}
67
68/// Apply transaction options to a transaction request for ERC20 operations.
69fn 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    // Apply Tempo-specific options
95    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
104/// Send an ERC20 transaction, handling Tempo transactions specially if needed
105///
106/// TODO: Remove this temporary helper when we migrate to FoundryNetwork/FoundryTransactionRequest.
107async 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    // Same as in SendTxArgs::run(), Tempo transactions need to be signed locally and sent as raw
114    // transactions
115    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        // Encode and send raw
125        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            // For sync mode, we already have the hash, just wait for receipt
136            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    // Use the normal cast_send path for non-Tempo transactions
146    cast_send(provider, tx, send_tx.cast_async, send_tx.sync, send_tx.confirmations, timeout).await
147}
148/// Interact with ERC20 tokens.
149#[derive(Debug, Parser, Clone)]
150pub enum Erc20Subcommand {
151    /// Query ERC20 token balance.
152    #[command(visible_alias = "b")]
153    Balance {
154        /// The ERC20 token contract address.
155        #[arg(value_parser = NameOrAddress::from_str)]
156        token: NameOrAddress,
157
158        /// The owner to query balance for.
159        #[arg(value_parser = NameOrAddress::from_str)]
160        owner: NameOrAddress,
161
162        /// The block height to query at.
163        #[arg(long, short = 'B')]
164        block: Option<BlockId>,
165
166        #[command(flatten)]
167        rpc: RpcOpts,
168    },
169
170    /// Transfer ERC20 tokens.
171    #[command(visible_aliases = ["t", "send"])]
172    Transfer {
173        /// The ERC20 token contract address.
174        #[arg(value_parser = NameOrAddress::from_str)]
175        token: NameOrAddress,
176
177        /// The recipient address.
178        #[arg(value_parser = NameOrAddress::from_str)]
179        to: NameOrAddress,
180
181        /// The amount to transfer.
182        amount: String,
183
184        #[command(flatten)]
185        send_tx: SendTxOpts,
186
187        #[command(flatten)]
188        tx: Erc20TxOpts,
189    },
190
191    /// Approve ERC20 token spending.
192    #[command(visible_alias = "a")]
193    Approve {
194        /// The ERC20 token contract address.
195        #[arg(value_parser = NameOrAddress::from_str)]
196        token: NameOrAddress,
197
198        /// The spender address.
199        #[arg(value_parser = NameOrAddress::from_str)]
200        spender: NameOrAddress,
201
202        /// The amount to approve.
203        amount: String,
204
205        #[command(flatten)]
206        send_tx: SendTxOpts,
207
208        #[command(flatten)]
209        tx: Erc20TxOpts,
210    },
211
212    /// Query ERC20 token allowance.
213    #[command(visible_alias = "al")]
214    Allowance {
215        /// The ERC20 token contract address.
216        #[arg(value_parser = NameOrAddress::from_str)]
217        token: NameOrAddress,
218
219        /// The owner address.
220        #[arg(value_parser = NameOrAddress::from_str)]
221        owner: NameOrAddress,
222
223        /// The spender address.
224        #[arg(value_parser = NameOrAddress::from_str)]
225        spender: NameOrAddress,
226
227        /// The block height to query at.
228        #[arg(long, short = 'B')]
229        block: Option<BlockId>,
230
231        #[command(flatten)]
232        rpc: RpcOpts,
233    },
234
235    /// Query ERC20 token name.
236    #[command(visible_alias = "n")]
237    Name {
238        /// The ERC20 token contract address.
239        #[arg(value_parser = NameOrAddress::from_str)]
240        token: NameOrAddress,
241
242        /// The block height to query at.
243        #[arg(long, short = 'B')]
244        block: Option<BlockId>,
245
246        #[command(flatten)]
247        rpc: RpcOpts,
248    },
249
250    /// Query ERC20 token symbol.
251    #[command(visible_alias = "s")]
252    Symbol {
253        /// The ERC20 token contract address.
254        #[arg(value_parser = NameOrAddress::from_str)]
255        token: NameOrAddress,
256
257        /// The block height to query at.
258        #[arg(long, short = 'B')]
259        block: Option<BlockId>,
260
261        #[command(flatten)]
262        rpc: RpcOpts,
263    },
264
265    /// Query ERC20 token decimals.
266    #[command(visible_alias = "d")]
267    Decimals {
268        /// The ERC20 token contract address.
269        #[arg(value_parser = NameOrAddress::from_str)]
270        token: NameOrAddress,
271
272        /// The block height to query at.
273        #[arg(long, short = 'B')]
274        block: Option<BlockId>,
275
276        #[command(flatten)]
277        rpc: RpcOpts,
278    },
279
280    /// Query ERC20 token total supply.
281    #[command(visible_alias = "ts")]
282    TotalSupply {
283        /// The ERC20 token contract address.
284        #[arg(value_parser = NameOrAddress::from_str)]
285        token: NameOrAddress,
286
287        /// The block height to query at.
288        #[arg(long, short = 'B')]
289        block: Option<BlockId>,
290
291        #[command(flatten)]
292        rpc: RpcOpts,
293    },
294
295    /// Mint ERC20 tokens (if the token supports minting).
296    #[command(visible_alias = "m")]
297    Mint {
298        /// The ERC20 token contract address.
299        #[arg(value_parser = NameOrAddress::from_str)]
300        token: NameOrAddress,
301
302        /// The recipient address.
303        #[arg(value_parser = NameOrAddress::from_str)]
304        to: NameOrAddress,
305
306        /// The amount to mint.
307        amount: String,
308
309        #[command(flatten)]
310        send_tx: SendTxOpts,
311
312        #[command(flatten)]
313        tx: Erc20TxOpts,
314    },
315
316    /// Burn ERC20 tokens.
317    #[command(visible_alias = "bu")]
318    Burn {
319        /// The ERC20 token contract address.
320        #[arg(value_parser = NameOrAddress::from_str)]
321        token: NameOrAddress,
322
323        /// The amount to burn.
324        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            // Read-only
355            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            // State-changing
450            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 transaction options using helper
457                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 transaction options using helper
478                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 transaction options using helper
499                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 transaction options using helper
520                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}