Skip to main content

cast/cmd/
erc20.rs

1use std::{str::FromStr, time::Duration};
2
3use crate::{cmd::send::cast_send, format_uint_exp, tx::SendTxOpts};
4use alloy_consensus::{SignableTransaction, Signed};
5use alloy_eips::BlockId;
6use alloy_ens::NameOrAddress;
7use alloy_network::{AnyNetwork, EthereumWallet, Network, TransactionBuilder};
8use alloy_primitives::{U64, U256};
9use alloy_provider::{Provider, fillers::RecommendedFillers};
10use alloy_signer::Signature;
11use alloy_sol_types::sol;
12use clap::{Args, Parser};
13use foundry_cli::{
14    opts::{RpcOpts, TempoOpts},
15    utils::{LoadConfig, get_chain, get_provider},
16};
17use foundry_common::{
18    fmt::{UIfmt, UIfmtReceiptExt},
19    provider::{ProviderBuilder, RetryProviderWithSigner},
20    shell,
21};
22#[doc(hidden)]
23pub use foundry_config::{Chain, utils::*};
24use foundry_primitives::FoundryTransactionBuilder;
25use tempo_alloy::TempoNetwork;
26
27sol! {
28    #[sol(rpc)]
29    interface IERC20 {
30        #[derive(Debug)]
31        function name() external view returns (string);
32        function symbol() external view returns (string);
33        function decimals() external view returns (uint8);
34        function totalSupply() external view returns (uint256);
35        function balanceOf(address owner) external view returns (uint256);
36        function transfer(address to, uint256 amount) external returns (bool);
37        function approve(address spender, uint256 amount) external returns (bool);
38        function allowance(address owner, address spender) external view returns (uint256);
39        function mint(address to, uint256 amount) external;
40        function burn(uint256 amount) external;
41    }
42}
43
44/// Transaction options for ERC20 operations.
45///
46/// This struct contains only the transaction options relevant to ERC20 token interactions
47#[derive(Debug, Clone, Args)]
48pub struct Erc20TxOpts {
49    /// Gas limit for the transaction.
50    #[arg(long, env = "ETH_GAS_LIMIT")]
51    pub gas_limit: Option<U256>,
52
53    /// Gas price for legacy transactions, or max fee per gas for EIP1559 transactions.
54    #[arg(long, env = "ETH_GAS_PRICE")]
55    pub gas_price: Option<U256>,
56
57    /// Max priority fee per gas for EIP1559 transactions.
58    #[arg(long, env = "ETH_PRIORITY_GAS_PRICE")]
59    pub priority_gas_price: Option<U256>,
60
61    /// Nonce for the transaction.
62    #[arg(long)]
63    pub nonce: Option<U64>,
64
65    #[command(flatten)]
66    pub tempo: TempoOpts,
67}
68
69/// Creates a provider with wallet for signing transactions locally.
70pub(crate) async fn get_provider_with_wallet<N: Network + RecommendedFillers>(
71    tx_opts: &SendTxOpts,
72) -> eyre::Result<RetryProviderWithSigner<N>>
73where
74    N::TxEnvelope: From<Signed<N::UnsignedTx>>,
75    N::UnsignedTx: SignableTransaction<Signature>,
76{
77    let config = tx_opts.eth.load_config()?;
78    let signer = tx_opts.eth.wallet.signer().await?;
79    let wallet = EthereumWallet::from(signer);
80    let provider = ProviderBuilder::<N>::from_config(&config)?.build_with_wallet(wallet)?;
81    if let Some(interval) = tx_opts.poll_interval {
82        provider.client().set_poll_interval(Duration::from_secs(interval))
83    }
84    Ok(provider)
85}
86
87impl Erc20TxOpts {
88    /// Applies gas, fee, nonce, and Tempo options to a transaction request.
89    fn apply<N: Network>(&self, tx: &mut N::TransactionRequest, legacy: bool)
90    where
91        N::TransactionRequest: FoundryTransactionBuilder<N>,
92    {
93        if let Some(gas_limit) = self.gas_limit {
94            tx.set_gas_limit(gas_limit.to());
95        }
96
97        if let Some(gas_price) = self.gas_price {
98            if legacy {
99                tx.set_gas_price(gas_price.to());
100            } else {
101                tx.set_max_fee_per_gas(gas_price.to());
102            }
103        }
104
105        if !legacy && let Some(priority_fee) = self.priority_gas_price {
106            tx.set_max_priority_fee_per_gas(priority_fee.to());
107        }
108
109        self.tempo.apply::<N>(tx, self.nonce.map(|n| n.to()));
110    }
111}
112
113/// Interact with ERC20 tokens.
114#[derive(Debug, Parser, Clone)]
115pub enum Erc20Subcommand {
116    /// Query ERC20 token balance.
117    #[command(visible_alias = "b")]
118    Balance {
119        /// The ERC20 token contract address.
120        #[arg(value_parser = NameOrAddress::from_str)]
121        token: NameOrAddress,
122
123        /// The owner to query balance for.
124        #[arg(value_parser = NameOrAddress::from_str)]
125        owner: NameOrAddress,
126
127        /// The block height to query at.
128        #[arg(long, short = 'B')]
129        block: Option<BlockId>,
130
131        #[command(flatten)]
132        rpc: RpcOpts,
133    },
134
135    /// Transfer ERC20 tokens.
136    #[command(visible_aliases = ["t", "send"])]
137    Transfer {
138        /// The ERC20 token contract address.
139        #[arg(value_parser = NameOrAddress::from_str)]
140        token: NameOrAddress,
141
142        /// The recipient address.
143        #[arg(value_parser = NameOrAddress::from_str)]
144        to: NameOrAddress,
145
146        /// The amount to transfer.
147        amount: String,
148
149        #[command(flatten)]
150        send_tx: SendTxOpts,
151
152        #[command(flatten)]
153        tx: Erc20TxOpts,
154    },
155
156    /// Approve ERC20 token spending.
157    #[command(visible_alias = "a")]
158    Approve {
159        /// The ERC20 token contract address.
160        #[arg(value_parser = NameOrAddress::from_str)]
161        token: NameOrAddress,
162
163        /// The spender address.
164        #[arg(value_parser = NameOrAddress::from_str)]
165        spender: NameOrAddress,
166
167        /// The amount to approve.
168        amount: String,
169
170        #[command(flatten)]
171        send_tx: SendTxOpts,
172
173        #[command(flatten)]
174        tx: Erc20TxOpts,
175    },
176
177    /// Query ERC20 token allowance.
178    #[command(visible_alias = "al")]
179    Allowance {
180        /// The ERC20 token contract address.
181        #[arg(value_parser = NameOrAddress::from_str)]
182        token: NameOrAddress,
183
184        /// The owner address.
185        #[arg(value_parser = NameOrAddress::from_str)]
186        owner: NameOrAddress,
187
188        /// The spender address.
189        #[arg(value_parser = NameOrAddress::from_str)]
190        spender: NameOrAddress,
191
192        /// The block height to query at.
193        #[arg(long, short = 'B')]
194        block: Option<BlockId>,
195
196        #[command(flatten)]
197        rpc: RpcOpts,
198    },
199
200    /// Query ERC20 token name.
201    #[command(visible_alias = "n")]
202    Name {
203        /// The ERC20 token contract address.
204        #[arg(value_parser = NameOrAddress::from_str)]
205        token: NameOrAddress,
206
207        /// The block height to query at.
208        #[arg(long, short = 'B')]
209        block: Option<BlockId>,
210
211        #[command(flatten)]
212        rpc: RpcOpts,
213    },
214
215    /// Query ERC20 token symbol.
216    #[command(visible_alias = "s")]
217    Symbol {
218        /// The ERC20 token contract address.
219        #[arg(value_parser = NameOrAddress::from_str)]
220        token: NameOrAddress,
221
222        /// The block height to query at.
223        #[arg(long, short = 'B')]
224        block: Option<BlockId>,
225
226        #[command(flatten)]
227        rpc: RpcOpts,
228    },
229
230    /// Query ERC20 token decimals.
231    #[command(visible_alias = "d")]
232    Decimals {
233        /// The ERC20 token contract address.
234        #[arg(value_parser = NameOrAddress::from_str)]
235        token: NameOrAddress,
236
237        /// The block height to query at.
238        #[arg(long, short = 'B')]
239        block: Option<BlockId>,
240
241        #[command(flatten)]
242        rpc: RpcOpts,
243    },
244
245    /// Query ERC20 token total supply.
246    #[command(visible_alias = "ts")]
247    TotalSupply {
248        /// The ERC20 token contract address.
249        #[arg(value_parser = NameOrAddress::from_str)]
250        token: NameOrAddress,
251
252        /// The block height to query at.
253        #[arg(long, short = 'B')]
254        block: Option<BlockId>,
255
256        #[command(flatten)]
257        rpc: RpcOpts,
258    },
259
260    /// Mint ERC20 tokens (if the token supports minting).
261    #[command(visible_alias = "m")]
262    Mint {
263        /// The ERC20 token contract address.
264        #[arg(value_parser = NameOrAddress::from_str)]
265        token: NameOrAddress,
266
267        /// The recipient address.
268        #[arg(value_parser = NameOrAddress::from_str)]
269        to: NameOrAddress,
270
271        /// The amount to mint.
272        amount: String,
273
274        #[command(flatten)]
275        send_tx: SendTxOpts,
276
277        #[command(flatten)]
278        tx: Erc20TxOpts,
279    },
280
281    /// Burn ERC20 tokens.
282    #[command(visible_alias = "bu")]
283    Burn {
284        /// The ERC20 token contract address.
285        #[arg(value_parser = NameOrAddress::from_str)]
286        token: NameOrAddress,
287
288        /// The amount to burn.
289        amount: String,
290
291        #[command(flatten)]
292        send_tx: SendTxOpts,
293
294        #[command(flatten)]
295        tx: Erc20TxOpts,
296    },
297}
298
299impl Erc20Subcommand {
300    fn rpc_opts(&self) -> &RpcOpts {
301        match self {
302            Self::Allowance { rpc, .. } => rpc,
303            Self::Approve { send_tx, .. } => &send_tx.eth.rpc,
304            Self::Balance { rpc, .. } => rpc,
305            Self::Transfer { send_tx, .. } => &send_tx.eth.rpc,
306            Self::Name { rpc, .. } => rpc,
307            Self::Symbol { rpc, .. } => rpc,
308            Self::Decimals { rpc, .. } => rpc,
309            Self::TotalSupply { rpc, .. } => rpc,
310            Self::Mint { send_tx, .. } => &send_tx.eth.rpc,
311            Self::Burn { send_tx, .. } => &send_tx.eth.rpc,
312        }
313    }
314
315    fn erc20_opts(&self) -> Option<&Erc20TxOpts> {
316        match self {
317            Self::Approve { tx, .. }
318            | Self::Transfer { tx, .. }
319            | Self::Mint { tx, .. }
320            | Self::Burn { tx, .. } => Some(tx),
321            Self::Allowance { .. }
322            | Self::Balance { .. }
323            | Self::Name { .. }
324            | Self::Symbol { .. }
325            | Self::Decimals { .. }
326            | Self::TotalSupply { .. } => None,
327        }
328    }
329
330    pub async fn run(self) -> eyre::Result<()> {
331        if let Some(erc20) = self.erc20_opts()
332            && erc20.tempo.is_tempo()
333        {
334            self.run_generic::<TempoNetwork>().await
335        } else {
336            self.run_generic::<AnyNetwork>().await
337        }
338    }
339
340    pub async fn run_generic<N: Network + RecommendedFillers>(self) -> eyre::Result<()>
341    where
342        N::TxEnvelope: From<Signed<N::UnsignedTx>>,
343        N::UnsignedTx: SignableTransaction<Signature>,
344        N::TransactionRequest: FoundryTransactionBuilder<N>,
345        N::ReceiptResponse: UIfmt + UIfmtReceiptExt,
346    {
347        let config = self.rpc_opts().load_config()?;
348
349        match self {
350            // Read-only
351            Self::Allowance { token, owner, spender, block, .. } => {
352                let provider = get_provider(&config)?;
353                let token = token.resolve(&provider).await?;
354                let owner = owner.resolve(&provider).await?;
355                let spender = spender.resolve(&provider).await?;
356
357                let allowance = IERC20::new(token, &provider)
358                    .allowance(owner, spender)
359                    .block(block.unwrap_or_default())
360                    .call()
361                    .await?;
362
363                if shell::is_json() {
364                    sh_println!("{}", serde_json::to_string(&allowance.to_string())?)?
365                } else {
366                    sh_println!("{}", format_uint_exp(allowance))?
367                }
368            }
369            Self::Balance { token, owner, block, .. } => {
370                let provider = get_provider(&config)?;
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
380                if shell::is_json() {
381                    sh_println!("{}", serde_json::to_string(&balance.to_string())?)?
382                } else {
383                    sh_println!("{}", format_uint_exp(balance))?
384                }
385            }
386            Self::Name { token, block, .. } => {
387                let provider = get_provider(&config)?;
388                let token = token.resolve(&provider).await?;
389
390                let name = IERC20::new(token, &provider)
391                    .name()
392                    .block(block.unwrap_or_default())
393                    .call()
394                    .await?;
395
396                if shell::is_json() {
397                    sh_println!("{}", serde_json::to_string(&name)?)?
398                } else {
399                    sh_println!("{}", name)?
400                }
401            }
402            Self::Symbol { token, block, .. } => {
403                let provider = get_provider(&config)?;
404                let token = token.resolve(&provider).await?;
405
406                let symbol = IERC20::new(token, &provider)
407                    .symbol()
408                    .block(block.unwrap_or_default())
409                    .call()
410                    .await?;
411
412                if shell::is_json() {
413                    sh_println!("{}", serde_json::to_string(&symbol)?)?
414                } else {
415                    sh_println!("{}", symbol)?
416                }
417            }
418            Self::Decimals { token, block, .. } => {
419                let provider = get_provider(&config)?;
420                let token = token.resolve(&provider).await?;
421
422                let decimals = IERC20::new(token, &provider)
423                    .decimals()
424                    .block(block.unwrap_or_default())
425                    .call()
426                    .await?;
427                if shell::is_json() {
428                    sh_println!("{}", serde_json::to_string(&decimals)?)?
429                } else {
430                    sh_println!("{}", decimals)?
431                }
432            }
433            Self::TotalSupply { token, block, .. } => {
434                let provider = get_provider(&config)?;
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 = get_provider_with_wallet::<N>(&send_tx).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                tx_opts.apply::<N>(&mut tx, get_chain(config.chain, &provider).await?.is_legacy());
457
458                cast_send(
459                    provider,
460                    tx,
461                    send_tx.cast_async,
462                    send_tx.sync,
463                    send_tx.confirmations,
464                    send_tx.timeout.unwrap_or(config.transaction_timeout),
465                )
466                .await?
467            }
468            Self::Approve { token, spender, amount, send_tx, tx: tx_opts, .. } => {
469                let provider = get_provider_with_wallet::<N>(&send_tx).await?;
470                let mut tx = IERC20::new(token.resolve(&provider).await?, &provider)
471                    .approve(spender.resolve(&provider).await?, U256::from_str(&amount)?)
472                    .into_transaction_request();
473
474                tx_opts.apply::<N>(&mut tx, get_chain(config.chain, &provider).await?.is_legacy());
475
476                cast_send(
477                    provider,
478                    tx,
479                    send_tx.cast_async,
480                    send_tx.sync,
481                    send_tx.confirmations,
482                    send_tx.timeout.unwrap_or(config.transaction_timeout),
483                )
484                .await?
485            }
486            Self::Mint { token, to, amount, send_tx, tx: tx_opts, .. } => {
487                let provider = get_provider_with_wallet::<N>(&send_tx).await?;
488                let mut tx = IERC20::new(token.resolve(&provider).await?, &provider)
489                    .mint(to.resolve(&provider).await?, U256::from_str(&amount)?)
490                    .into_transaction_request();
491
492                tx_opts.apply::<N>(&mut tx, get_chain(config.chain, &provider).await?.is_legacy());
493
494                cast_send(
495                    provider,
496                    tx,
497                    send_tx.cast_async,
498                    send_tx.sync,
499                    send_tx.confirmations,
500                    send_tx.timeout.unwrap_or(config.transaction_timeout),
501                )
502                .await?
503            }
504            Self::Burn { token, amount, send_tx, tx: tx_opts, .. } => {
505                let provider = get_provider_with_wallet::<N>(&send_tx).await?;
506                let mut tx = IERC20::new(token.resolve(&provider).await?, &provider)
507                    .burn(U256::from_str(&amount)?)
508                    .into_transaction_request();
509
510                tx_opts.apply::<N>(&mut tx, get_chain(config.chain, &provider).await?.is_legacy());
511
512                cast_send(
513                    provider,
514                    tx,
515                    send_tx.cast_async,
516                    send_tx.sync,
517                    send_tx.confirmations,
518                    send_tx.timeout.unwrap_or(config.transaction_timeout),
519                )
520                .await?
521            }
522        };
523        Ok(())
524    }
525}