Skip to content

Commit 6062254

Browse files
authored
fix(cast): sign erc20 transactions locally before broadcast (#12372)
* fix(cast): sign erc20 transactions locally before broadcast * fix: whitespaces * style: rmv cmnt * chore: refactor to reduce duplication
1 parent ad78ed2 commit 6062254

File tree

5 files changed

+90
-145
lines changed

5 files changed

+90
-145
lines changed

crates/cast/src/args.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::{
22
Cast, SimpleCast,
3+
cmd::erc20::IERC20,
34
opts::{Cast as CastArgs, CastSubcommand, ToBaseArgs},
45
traces::identifier::SignaturesIdentifier,
56
};
@@ -310,8 +311,12 @@ pub async fn run_command(args: CastArgs) -> Result<()> {
310311

311312
match erc20 {
312313
Some(token) => {
313-
let balance =
314-
Cast::new(&provider).erc20_balance(token, account_addr, block).await?;
314+
let balance = IERC20::new(token, &provider)
315+
.balanceOf(account_addr)
316+
.block(block.unwrap_or_default())
317+
.call()
318+
.await?;
319+
315320
sh_warn!("--erc20 flag is deprecated, use `cast erc20 balance` instead")?;
316321
sh_println!("{}", format_uint_exp(balance))?
317322
}

crates/cast/src/cmd/erc20.rs

Lines changed: 65 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,31 @@
11
use std::str::FromStr;
22

3-
use crate::Cast;
3+
use crate::{format_uint_exp, tx::signing_provider};
44
use alloy_eips::BlockId;
55
use alloy_ens::NameOrAddress;
66
use alloy_primitives::U256;
7+
use alloy_sol_types::sol;
78
use clap::Parser;
89
use foundry_cli::{
910
opts::RpcOpts,
1011
utils::{LoadConfig, get_provider},
1112
};
12-
use foundry_common::fmt::format_uint_exp;
1313
use foundry_wallets::WalletOpts;
1414

1515
#[doc(hidden)]
1616
pub use foundry_config::utils::*;
1717

18+
sol! {
19+
#[sol(rpc)]
20+
interface IERC20 {
21+
#[derive(Debug)]
22+
function balanceOf(address owner) external view returns (uint256);
23+
function transfer(address to, uint256 amount) external returns (bool);
24+
function approve(address spender, uint256 amount) external returns (bool);
25+
function allowance(address owner, address spender) external view returns (uint256);
26+
}
27+
}
28+
1829
/// Interact with ERC20 tokens.
1930
#[derive(Debug, Parser, Clone)]
2031
pub enum Erc20Subcommand {
@@ -51,10 +62,6 @@ pub enum Erc20Subcommand {
5162
/// The amount to transfer.
5263
amount: String,
5364

54-
/// The block height to query at.
55-
#[arg(long, short = 'B')]
56-
block: Option<BlockId>,
57-
5865
#[command(flatten)]
5966
rpc: RpcOpts,
6067

@@ -76,10 +83,6 @@ pub enum Erc20Subcommand {
7683
/// The amount to approve.
7784
amount: String,
7885

79-
/// The block height to query at.
80-
#[arg(long, short = 'B')]
81-
block: Option<BlockId>,
82-
8386
#[command(flatten)]
8487
rpc: RpcOpts,
8588

@@ -112,60 +115,65 @@ pub enum Erc20Subcommand {
112115
}
113116

114117
impl Erc20Subcommand {
118+
fn rpc(&self) -> &RpcOpts {
119+
match self {
120+
Self::Allowance { rpc, .. } => rpc,
121+
Self::Approve { rpc, .. } => rpc,
122+
Self::Balance { rpc, .. } => rpc,
123+
Self::Transfer { rpc, .. } => rpc,
124+
}
125+
}
126+
115127
pub async fn run(self) -> eyre::Result<()> {
128+
let config = self.rpc().load_config()?;
129+
let provider = get_provider(&config)?;
130+
116131
match self {
117-
Self::Balance { token, owner, block, rpc } => {
118-
let config = rpc.load_config()?;
119-
let provider = get_provider(&config)?;
120-
let owner_addr = owner.resolve(&provider).await?;
121-
let token_addr = token.resolve(&provider).await?;
122-
let balance =
123-
Cast::new(&provider).erc20_balance(token_addr, owner_addr, block).await?;
124-
sh_println!("{}", format_uint_exp(balance))?
125-
}
126-
Self::Transfer { token, to, amount, block: _, rpc, wallet } => {
127-
let config = rpc.load_config()?;
128-
let provider = get_provider(&config)?;
129-
let to_addr = to.resolve(&provider).await?;
130-
let token_addr = token.resolve(&provider).await?;
131-
let amount_u256 = U256::from_str(&amount)?;
132-
133-
// Create signer from wallet options if available
134-
let signer = wallet.signer().await.ok();
135-
136-
let tx_hash = Cast::new(&provider)
137-
.erc20_transfer(token_addr, to_addr, amount_u256, signer)
132+
// Read-only
133+
Self::Allowance { token, owner, spender, block, .. } => {
134+
let token = token.resolve(&provider).await?;
135+
let owner = owner.resolve(&provider).await?;
136+
let spender = spender.resolve(&provider).await?;
137+
138+
let allowance = IERC20::new(token, &provider)
139+
.allowance(owner, spender)
140+
.block(block.unwrap_or_default())
141+
.call()
138142
.await?;
139-
sh_println!("{}", tx_hash)?
143+
144+
sh_println!("{}", format_uint_exp(allowance))?
140145
}
141-
Self::Approve { token, spender, amount, block: _, rpc, wallet } => {
142-
let config = rpc.load_config()?;
143-
let provider = get_provider(&config)?;
144-
let spender_addr = spender.resolve(&provider).await?;
145-
let token_addr = token.resolve(&provider).await?;
146-
let amount_u256 = U256::from_str(&amount)?;
147-
148-
// Create signer from wallet options if available
149-
let signer = wallet.signer().await.ok();
150-
151-
let tx_hash = Cast::new(&provider)
152-
.erc20_approve(token_addr, spender_addr, amount_u256, signer)
146+
Self::Balance { token, owner, block, .. } => {
147+
let token = token.resolve(&provider).await?;
148+
let owner = owner.resolve(&provider).await?;
149+
150+
let balance = IERC20::new(token, &provider)
151+
.balanceOf(owner)
152+
.block(block.unwrap_or_default())
153+
.call()
153154
.await?;
154-
sh_println!("{}", tx_hash)?
155+
sh_println!("{}", format_uint_exp(balance))?
155156
}
156-
Self::Allowance { token, owner, spender, block, rpc } => {
157-
let config = rpc.load_config()?;
158-
let provider = get_provider(&config)?;
159-
let owner_addr = owner.resolve(&provider).await?;
160-
let spender_addr = spender.resolve(&provider).await?;
161-
let token_addr = token.resolve(&provider).await?;
162-
163-
let allowance = Cast::new(&provider)
164-
.erc20_allowance(token_addr, owner_addr, spender_addr, block)
165-
.await?;
166-
sh_println!("{}", format_uint_exp(allowance))?
157+
// State-changing
158+
Self::Transfer { token, to, amount, wallet, .. } => {
159+
let token = token.resolve(&provider).await?;
160+
let to = to.resolve(&provider).await?;
161+
let amount = U256::from_str(&amount)?;
162+
163+
let provider = signing_provider(wallet, &provider).await?;
164+
let tx = IERC20::new(token, &provider).transfer(to, amount).send().await?;
165+
sh_println!("{}", tx.tx_hash())?
167166
}
168-
}
167+
Self::Approve { token, spender, amount, wallet, .. } => {
168+
let token = token.resolve(&provider).await?;
169+
let spender = spender.resolve(&provider).await?;
170+
let amount = U256::from_str(&amount)?;
171+
172+
let provider = signing_provider(wallet, &provider).await?;
173+
let tx = IERC20::new(token, &provider).approve(spender, amount).send().await?;
174+
sh_println!("{}", tx.tx_hash())?
175+
}
176+
};
169177
Ok(())
170178
}
171179
}

crates/cast/src/lib.rs

Lines changed: 0 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ use alloy_rpc_types::{
2121
BlockId, BlockNumberOrTag, BlockOverrides, Filter, TransactionRequest, state::StateOverride,
2222
};
2323
use alloy_serde::WithOtherFields;
24-
use alloy_signer::Signer;
25-
use alloy_sol_types::sol;
2624
use base::{Base, NumberWithBase, ToBase};
2725
use chrono::DateTime;
2826
use eyre::{Context, ContextCompat, OptionExt, Result};
@@ -74,17 +72,6 @@ extern crate foundry_common;
7472

7573
// TODO: CastContract with common contract initializers? Same for CastProviders?
7674

77-
sol! {
78-
#[sol(rpc)]
79-
interface IERC20 {
80-
#[derive(Debug)]
81-
function balanceOf(address owner) external view returns (uint256);
82-
function transfer(address to, uint256 amount) external returns (bool);
83-
function approve(address spender, uint256 amount) external returns (bool);
84-
function allowance(address owner, address spender) external view returns (uint256);
85-
}
86-
}
87-
8875
pub struct Cast<P> {
8976
provider: P,
9077
}
@@ -1115,69 +1102,6 @@ impl<P: Provider<AnyNetwork>> Cast<P> {
11151102

11161103
Ok(())
11171104
}
1118-
1119-
pub async fn erc20_balance(
1120-
&self,
1121-
token: Address,
1122-
owner: Address,
1123-
block: Option<BlockId>,
1124-
) -> Result<U256> {
1125-
Ok(IERC20::new(token, &self.provider)
1126-
.balanceOf(owner)
1127-
.block(block.unwrap_or_default())
1128-
.call()
1129-
.await?)
1130-
}
1131-
1132-
pub async fn erc20_allowance(
1133-
&self,
1134-
token: Address,
1135-
owner: Address,
1136-
spender: Address,
1137-
block: Option<BlockId>,
1138-
) -> Result<U256> {
1139-
Ok(IERC20::new(token, &self.provider)
1140-
.allowance(owner, spender)
1141-
.block(block.unwrap_or_default())
1142-
.call()
1143-
.await?)
1144-
}
1145-
1146-
pub async fn erc20_transfer(
1147-
&self,
1148-
token: Address,
1149-
to: Address,
1150-
amount: U256,
1151-
signer: Option<foundry_wallets::WalletSigner>,
1152-
) -> Result<TxHash> {
1153-
let contract = IERC20::new(token, &self.provider);
1154-
1155-
if let Some(signer) = signer {
1156-
let tx = contract.transfer(to, amount).from(signer.address()).send().await?;
1157-
Ok(*tx.tx_hash())
1158-
} else {
1159-
let tx = contract.transfer(to, amount).send().await?;
1160-
Ok(*tx.tx_hash())
1161-
}
1162-
}
1163-
1164-
pub async fn erc20_approve(
1165-
&self,
1166-
token: Address,
1167-
spender: Address,
1168-
amount: U256,
1169-
signer: Option<foundry_wallets::WalletSigner>,
1170-
) -> Result<TxHash> {
1171-
let contract = IERC20::new(token, &self.provider);
1172-
1173-
if let Some(signer) = signer {
1174-
let tx = contract.approve(spender, amount).from(signer.address()).send().await?;
1175-
Ok(*tx.tx_hash())
1176-
} else {
1177-
let tx = contract.approve(spender, amount).send().await?;
1178-
Ok(*tx.tx_hash())
1179-
}
1180-
}
11811105
}
11821106

11831107
pub struct SimpleCast;

crates/cast/src/tx.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ use foundry_cli::{
1818
opts::{CliAuthorizationList, TransactionOpts},
1919
utils::{self, parse_function_args},
2020
};
21-
use foundry_common::fmt::format_tokens;
21+
use foundry_common::{
22+
fmt::format_tokens,
23+
provider::{RetryProvider, RetryProviderWithSigner},
24+
};
2225
use foundry_config::{Chain, Config};
2326
use foundry_wallets::{WalletOpts, WalletSigner};
2427
use itertools::Itertools;
@@ -471,3 +474,15 @@ async fn decode_execution_revert(data: &RawValue) -> Result<Option<String>> {
471474
}
472475
Ok(None)
473476
}
477+
478+
/// Creates a provider with wallet for signing transactions locally.
479+
pub async fn signing_provider(
480+
wallet: WalletOpts,
481+
provider: &RetryProvider,
482+
) -> eyre::Result<RetryProviderWithSigner> {
483+
let wallet = alloy_network::EthereumWallet::from(wallet.signer().await?);
484+
Ok(alloy_provider::ProviderBuilder::default()
485+
.with_recommended_fillers()
486+
.wallet(wallet)
487+
.connect_provider(provider.clone()))
488+
}

crates/cast/tests/cli/main.rs

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4304,11 +4304,8 @@ Deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
43044304
"--private-key",
43054305
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
43064306
])
4307-
.assert_success()
4308-
.stdout_eq(str![[r#"
4309-
0x60bfcd46dbda87681f35f82a93c1efa381bb12d3cdd8cee10e80b078a95619e8
4307+
.assert_success();
43104308

4311-
"#]]);
43124309
// new balance
43134310
cmd.cast_fuse()
43144311
.args([
@@ -4338,9 +4335,5 @@ Deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
43384335
"--private-key",
43394336
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
43404337
])
4341-
.assert_success()
4342-
.stdout_eq(str![[r#"
4343-
0x98712738efeb4030bd58a5bd13d25c650197548b56f38add80e689bfe55f1557
4344-
4345-
"#]]);
4338+
.assert_success();
43464339
});

0 commit comments

Comments
 (0)