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, 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/// Transaction options for ERC20 operations.
43///
44/// This struct contains only the transaction options relevant to ERC20 token interactions
45#[derive(Debug, Clone, Args)]
46pub struct Erc20TxOpts {
47    /// Gas limit for the transaction.
48    #[arg(long, env = "ETH_GAS_LIMIT")]
49    pub gas_limit: Option<U256>,
50
51    /// Gas price for legacy transactions, or max fee per gas for EIP1559 transactions.
52    #[arg(long, env = "ETH_GAS_PRICE")]
53    pub gas_price: Option<U256>,
54
55    /// Max priority fee per gas for EIP1559 transactions.
56    #[arg(long, env = "ETH_PRIORITY_GAS_PRICE")]
57    pub priority_gas_price: Option<U256>,
58
59    /// Nonce for the transaction.
60    #[arg(long)]
61    pub nonce: Option<U64>,
62
63    #[command(flatten)]
64    pub tempo: TempoOpts,
65}
66
67/// Apply transaction options to a transaction request for ERC20 operations.
68fn 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    // Apply Tempo-specific options
94    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
103/// Send an ERC20 transaction, handling Tempo transactions specially if needed
104///
105/// TODO: Remove this temporary helper when we migrate to FoundryNetwork/FoundryTransactionRequest.
106async 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    // Same as in SendTxArgs::run(), Tempo transactions need to be signed locally and sent as raw
113    // transactions
114    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        // Sign using NetworkWallet<FoundryNetwork>
122        let signed_tx = signer.sign_request(ftx).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                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            // State-changing
426            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 transaction options using helper
433                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 transaction options using helper
454                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 transaction options using helper
475                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 transaction options using helper
496                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}