Skip to main content

cast/cmd/
erc20.rs

1use foundry_common::fmt::format_uint_exp;
2use std::str::FromStr;
3
4use crate::{
5    cmd::{call_overrides::CallOverrideOpts, rpc_provider, send::SendTxArgs},
6    tx::{SendTxOpts, TxParams},
7};
8use alloy_eips::BlockId;
9use alloy_ens::NameOrAddress;
10use alloy_network::AnyNetwork;
11use alloy_primitives::{Address, U256};
12use alloy_sol_types::{SolCall, sol};
13use clap::Parser;
14use eyre::Result;
15use foundry_cli::{
16    json::{print_json_success, print_scalar},
17    opts::RpcOpts,
18};
19use foundry_common::{provider::RetryProvider, shell};
20
21mod permit;
22
23sol! {
24    #[sol(rpc)]
25    interface IERC20 {
26        event Transfer(address indexed from, address indexed to, uint256 value);
27
28        function name() external view returns (string);
29        function symbol() external view returns (string);
30        function decimals() external view returns (uint8);
31        function totalSupply() external view returns (uint256);
32        function balanceOf(address owner) external view returns (uint256);
33        function transfer(address to, uint256 amount) external returns (bool);
34        function approve(address spender, uint256 amount) external returns (bool);
35        function allowance(address owner, address spender) external view returns (uint256);
36        function mint(address to, uint256 amount) external;
37        function burn(uint256 amount) external;
38    }
39}
40
41/// Interact with ERC20 tokens.
42#[derive(Debug, Parser, Clone)]
43pub enum Erc20Subcommand {
44    /// Sign an ERC-2612 approval without sending a transaction, or submit it with --broadcast.
45    ///
46    /// The owner is the signing wallet. Amounts are in raw token units and the deadline is an
47    /// absolute Unix timestamp in seconds. This does not transfer tokens or deposit into a vault.
48    /// For deposits, the permit must target the underlying asset, not the vault's share token.
49    ///
50    /// By default stdout contains the 65-byte signature (r, s, v). With --json, it contains the
51    /// signature, permit calldata, owner, spender, value, nonce, deadline, token, and typed data.
52    /// With --broadcast, output follows cast send: a receipt, or a transaction hash with --async.
53    /// Anyone can submit the generated calldata to the token before the deadline.
54    /// Transaction options require --broadcast; --nonce selects the transaction nonce, while the
55    /// permit nonce is always read from the token.
56    ///
57    /// Uses EIP-5267 domain discovery when available. Otherwise uses name(), version "1", the
58    /// RPC chain ID, and the token address. --domain-name and --domain-version override the name
59    /// and version. The resulting domain must match DOMAIN_SEPARATOR() before signing.
60    /// DAI-style permits and Permit2 are not supported.
61    ///
62    /// Example:
63    /// ```text
64    /// cast erc20 permit $TOKEN $SPENDER 1000000 --deadline $DEADLINE \
65    ///     --account owner --rpc-url $RPC_URL --json
66    /// cast erc20 permit $TOKEN $SPENDER 1000000 --deadline $DEADLINE \
67    ///     --account owner --rpc-url $RPC_URL --broadcast --async
68    /// ```
69    #[command(verbatim_doc_comment)]
70    Permit(permit::PermitArgs),
71
72    /// Query ERC20 token balance.
73    #[command(visible_alias = "b")]
74    Balance {
75        /// The ERC20 token contract address.
76        #[arg(value_parser = NameOrAddress::from_str)]
77        token: NameOrAddress,
78
79        /// The owner to query balance for.
80        #[arg(value_parser = NameOrAddress::from_str)]
81        owner: NameOrAddress,
82
83        /// The block height to query at.
84        #[arg(long, short = 'B')]
85        block: Option<BlockId>,
86
87        #[command(flatten)]
88        rpc: RpcOpts,
89
90        #[command(flatten)]
91        overrides: CallOverrideOpts,
92    },
93
94    /// Transfer ERC20 tokens.
95    #[command(visible_aliases = ["t", "send"])]
96    Transfer {
97        /// The ERC20 token contract address.
98        #[arg(value_parser = NameOrAddress::from_str)]
99        token: NameOrAddress,
100
101        /// The recipient address.
102        #[arg(value_parser = NameOrAddress::from_str)]
103        to: NameOrAddress,
104
105        /// The amount to transfer.
106        amount: String,
107
108        #[command(flatten)]
109        send_tx: SendTxOpts,
110
111        #[command(flatten)]
112        tx: TxParams,
113    },
114
115    /// Approve ERC20 token spending.
116    #[command(visible_alias = "a")]
117    Approve {
118        /// The ERC20 token contract address.
119        #[arg(value_parser = NameOrAddress::from_str)]
120        token: NameOrAddress,
121
122        /// The spender address.
123        #[arg(value_parser = NameOrAddress::from_str)]
124        spender: NameOrAddress,
125
126        /// The amount to approve.
127        amount: String,
128
129        #[command(flatten)]
130        send_tx: SendTxOpts,
131
132        #[command(flatten)]
133        tx: TxParams,
134    },
135
136    /// Query ERC20 token allowance.
137    #[command(visible_alias = "al")]
138    Allowance {
139        /// The ERC20 token contract address.
140        #[arg(value_parser = NameOrAddress::from_str)]
141        token: NameOrAddress,
142
143        /// The owner address.
144        #[arg(value_parser = NameOrAddress::from_str)]
145        owner: NameOrAddress,
146
147        /// The spender address.
148        #[arg(value_parser = NameOrAddress::from_str)]
149        spender: NameOrAddress,
150
151        /// The block height to query at.
152        #[arg(long, short = 'B')]
153        block: Option<BlockId>,
154
155        #[command(flatten)]
156        rpc: RpcOpts,
157    },
158
159    /// Query ERC20 token name.
160    #[command(visible_alias = "n")]
161    Name {
162        /// The ERC20 token contract address.
163        #[arg(value_parser = NameOrAddress::from_str)]
164        token: NameOrAddress,
165
166        /// The block height to query at.
167        #[arg(long, short = 'B')]
168        block: Option<BlockId>,
169
170        #[command(flatten)]
171        rpc: RpcOpts,
172    },
173
174    /// Query ERC20 token symbol.
175    #[command(visible_alias = "s")]
176    Symbol {
177        /// The ERC20 token contract address.
178        #[arg(value_parser = NameOrAddress::from_str)]
179        token: NameOrAddress,
180
181        /// The block height to query at.
182        #[arg(long, short = 'B')]
183        block: Option<BlockId>,
184
185        #[command(flatten)]
186        rpc: RpcOpts,
187    },
188
189    /// Query ERC20 token decimals.
190    #[command(visible_alias = "d")]
191    Decimals {
192        /// The ERC20 token contract address.
193        #[arg(value_parser = NameOrAddress::from_str)]
194        token: NameOrAddress,
195
196        /// The block height to query at.
197        #[arg(long, short = 'B')]
198        block: Option<BlockId>,
199
200        #[command(flatten)]
201        rpc: RpcOpts,
202    },
203
204    /// Query ERC20 token total supply.
205    #[command(visible_alias = "ts")]
206    TotalSupply {
207        /// The ERC20 token contract address.
208        #[arg(value_parser = NameOrAddress::from_str)]
209        token: NameOrAddress,
210
211        /// The block height to query at.
212        #[arg(long, short = 'B')]
213        block: Option<BlockId>,
214
215        #[command(flatten)]
216        rpc: RpcOpts,
217    },
218
219    /// Mint ERC20 tokens (if the token supports minting).
220    #[command(visible_alias = "m")]
221    Mint {
222        /// The ERC20 token contract address.
223        #[arg(value_parser = NameOrAddress::from_str)]
224        token: NameOrAddress,
225
226        /// The recipient address.
227        #[arg(value_parser = NameOrAddress::from_str)]
228        to: NameOrAddress,
229
230        /// The amount to mint.
231        amount: String,
232
233        #[command(flatten)]
234        send_tx: SendTxOpts,
235
236        #[command(flatten)]
237        tx: TxParams,
238    },
239
240    /// Burn ERC20 tokens.
241    #[command(visible_alias = "bu")]
242    Burn {
243        /// The ERC20 token contract address.
244        #[arg(value_parser = NameOrAddress::from_str)]
245        token: NameOrAddress,
246
247        /// The amount to burn.
248        amount: String,
249
250        #[command(flatten)]
251        send_tx: SendTxOpts,
252
253        #[command(flatten)]
254        tx: TxParams,
255    },
256}
257
258impl Erc20Subcommand {
259    pub async fn run(self) -> Result<()> {
260        match self {
261            Self::Permit(args) => args.run().await,
262            Self::Allowance { token, owner, spender, block, rpc } => {
263                let (provider, erc20) = token_at(&rpc, token).await?;
264                let owner = owner.resolve(&provider).await?;
265                let spender = spender.resolve(&provider).await?;
266                let allowance =
267                    erc20.allowance(owner, spender).block(block.unwrap_or_default()).call().await?;
268                print_amount(allowance)
269            }
270            Self::Balance { token, owner, block, rpc, overrides } => {
271                let (provider, erc20) = token_at(&rpc, token).await?;
272                let owner = owner.resolve(&provider).await?;
273                let call = erc20.balanceOf(owner).block(block.unwrap_or_default());
274                let balance = overrides.apply(call.call())?.await?;
275                print_scalar(balance.to_string())
276            }
277            Self::Name { token, block, rpc } => {
278                let (_, erc20) = token_at(&rpc, token).await?;
279                print_scalar(erc20.name().block(block.unwrap_or_default()).call().await?)
280            }
281            Self::Symbol { token, block, rpc } => {
282                let (_, erc20) = token_at(&rpc, token).await?;
283                print_scalar(erc20.symbol().block(block.unwrap_or_default()).call().await?)
284            }
285            Self::Decimals { token, block, rpc } => {
286                let (_, erc20) = token_at(&rpc, token).await?;
287                print_scalar(erc20.decimals().block(block.unwrap_or_default()).call().await?)
288            }
289            Self::TotalSupply { token, block, rpc } => {
290                let (_, erc20) = token_at(&rpc, token).await?;
291                print_amount(erc20.totalSupply().block(block.unwrap_or_default()).call().await?)
292            }
293            Self::Transfer { token, to, amount, send_tx, tx } => {
294                let to = resolve(&send_tx.eth.rpc, to).await?;
295                let call = IERC20::transferCall { to, amount: U256::from_str(&amount)? };
296                send(token, call, send_tx, tx).await
297            }
298            Self::Approve { token, spender, amount, send_tx, tx } => {
299                let spender = resolve(&send_tx.eth.rpc, spender).await?;
300                let call = IERC20::approveCall { spender, amount: U256::from_str(&amount)? };
301                send(token, call, send_tx, tx).await
302            }
303            Self::Mint { token, to, amount, send_tx, tx } => {
304                let to = resolve(&send_tx.eth.rpc, to).await?;
305                let call = IERC20::mintCall { to, amount: U256::from_str(&amount)? };
306                send(token, call, send_tx, tx).await
307            }
308            Self::Burn { token, amount, send_tx, tx } => {
309                send(token, IERC20::burnCall { amount: U256::from_str(&amount)? }, send_tx, tx)
310                    .await
311            }
312        }
313    }
314}
315
316async fn token_at(
317    rpc: &RpcOpts,
318    token: NameOrAddress,
319) -> Result<(RetryProvider, IERC20::IERC20Instance<RetryProvider, AnyNetwork>)> {
320    let provider = rpc_provider(rpc)?;
321    let token = token.resolve(&provider).await?;
322    Ok((provider.clone(), IERC20::new(token, provider)))
323}
324
325async fn resolve(rpc: &RpcOpts, account: NameOrAddress) -> Result<Address> {
326    Ok(account.resolve(&rpc_provider(rpc)?).await?)
327}
328
329async fn send(
330    token: NameOrAddress,
331    call: impl SolCall,
332    send_tx: SendTxOpts,
333    tx: TxParams,
334) -> Result<()> {
335    // Boxed to keep the large `cast send` future off this command's stack frame.
336    Box::pin(SendTxArgs::contract_call(token, call.abi_encode(), send_tx, tx).run()).await
337}
338
339/// Prints a token amount: the raw decimal string in JSON mode, exponent-annotated otherwise.
340pub(crate) fn print_amount(amount: U256) -> Result<()> {
341    if shell::is_json() {
342        print_json_success(amount.to_string())
343    } else {
344        sh_println!("{}", format_uint_exp(amount))
345    }
346}