Skip to content

Commit 70528b3

Browse files
zerosnacksrplusq
authored andcommitted
feat: add --broadcast flag to forge create, default to dry run mode (foundry-rs#9420)
* add --broadcast flag to forge create, default to dry run * nits * fix tests * add dry run tests incl --json * minor fixes, failing test due to minor bytecode difference
1 parent da930e8 commit 70528b3

File tree

2 files changed

+160
-6
lines changed

2 files changed

+160
-6
lines changed

crates/forge/bin/cmd/create.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ pub struct CreateArgs {
6161
)]
6262
constructor_args_path: Option<PathBuf>,
6363

64+
/// Broadcast the transaction.
65+
#[arg(long)]
66+
pub broadcast: bool,
67+
6468
/// Verify contract after creation.
6569
#[arg(long)]
6670
verify: bool,
@@ -155,6 +159,10 @@ impl CreateArgs {
155159
} else {
156160
provider.get_chain_id().await?
157161
};
162+
163+
// Whether to broadcast the transaction or not
164+
let dry_run = !self.broadcast;
165+
158166
if self.unlocked {
159167
// Deploy with unlocked account
160168
let sender = self.eth.wallet.from.expect("required");
@@ -167,6 +175,7 @@ impl CreateArgs {
167175
sender,
168176
config.transaction_timeout,
169177
id,
178+
dry_run,
170179
)
171180
.await
172181
} else {
@@ -185,6 +194,7 @@ impl CreateArgs {
185194
deployer,
186195
config.transaction_timeout,
187196
id,
197+
dry_run,
188198
)
189199
.await
190200
}
@@ -260,6 +270,7 @@ impl CreateArgs {
260270
deployer_address: Address,
261271
timeout: u64,
262272
id: ArtifactId,
273+
dry_run: bool,
263274
) -> Result<()> {
264275
let bin = bin.into_bytes().unwrap_or_else(|| {
265276
panic!("no bytecode found in bin object for {}", self.contract.name)
@@ -339,6 +350,30 @@ impl CreateArgs {
339350
self.verify_preflight_check(constructor_args.clone(), chain, &id).await?;
340351
}
341352

353+
if dry_run {
354+
if !shell::is_json() {
355+
sh_warn!("Dry run enabled, not broadcasting transaction\n")?;
356+
357+
sh_println!("Contract: {}", self.contract.name)?;
358+
sh_println!(
359+
"Transaction: {}",
360+
serde_json::to_string_pretty(&deployer.tx.clone())?
361+
)?;
362+
sh_println!("ABI: {}\n", serde_json::to_string_pretty(&abi)?)?;
363+
364+
sh_warn!("To broadcast this transaction, add --broadcast to the previous command. See forge create --help for more.")?;
365+
} else {
366+
let output = json!({
367+
"contract": self.contract.name,
368+
"transaction": &deployer.tx,
369+
"abi":&abi
370+
});
371+
sh_println!("{}", serde_json::to_string_pretty(&output)?)?;
372+
}
373+
374+
return Ok(());
375+
}
376+
342377
// Deploy the actual contract
343378
let (deployed_contract, receipt) = deployer.send_with_receipt().await?;
344379

@@ -349,7 +384,7 @@ impl CreateArgs {
349384
"deployedTo": address.to_string(),
350385
"transactionHash": receipt.transaction_hash
351386
});
352-
sh_println!("{output}")?;
387+
sh_println!("{}", serde_json::to_string_pretty(&output)?)?;
353388
} else {
354389
sh_println!("Deployer: {deployer_address}")?;
355390
sh_println!("Deployed to: {address}")?;

crates/forge/tests/cli/create.rs

Lines changed: 124 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ use anvil::{spawn, NodeConfig};
99
use foundry_compilers::artifacts::{remappings::Remapping, BytecodeHash};
1010
use foundry_config::Config;
1111
use foundry_test_utils::{
12-
forgetest, forgetest_async, str,
12+
forgetest, forgetest_async,
13+
snapbox::IntoData,
14+
str,
1315
util::{OutputExt, TestCommand, TestProject},
1416
};
1517
use std::str::FromStr;
@@ -145,6 +147,7 @@ forgetest_async!(can_create_template_contract, |prj, cmd| {
145147
let config = Config { bytecode_hash: BytecodeHash::None, ..Default::default() };
146148
prj.write_config(config);
147149

150+
// Dry-run without the `--broadcast` flag
148151
cmd.forge_fuse().args([
149152
"create",
150153
format!("./src/{TEMPLATE_CONTRACT}.sol:{TEMPLATE_CONTRACT}").as_str(),
@@ -154,20 +157,131 @@ forgetest_async!(can_create_template_contract, |prj, cmd| {
154157
pk.as_str(),
155158
]);
156159

160+
// Dry-run
157161
cmd.assert().stdout_eq(str![[r#"
158162
[COMPILING_FILES] with [SOLC_VERSION]
159163
[SOLC_VERSION] [ELAPSED]
160164
Compiler run successful!
161-
Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
162-
Deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
163-
[TX_HASH]
165+
Contract: Counter
166+
Transaction: {
167+
"from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
168+
"to": null,
169+
"maxFeePerGas": "0x77359401",
170+
"maxPriorityFeePerGas": "0x1",
171+
"gas": "0x17575",
172+
"input": "[..]",
173+
"nonce": "0x0",
174+
"chainId": "0x7a69"
175+
}
176+
ABI: [
177+
{
178+
"type": "function",
179+
"name": "increment",
180+
"inputs": [],
181+
"outputs": [],
182+
"stateMutability": "nonpayable"
183+
},
184+
{
185+
"type": "function",
186+
"name": "number",
187+
"inputs": [],
188+
"outputs": [
189+
{
190+
"name": "",
191+
"type": "uint256",
192+
"internalType": "uint256"
193+
}
194+
],
195+
"stateMutability": "view"
196+
},
197+
{
198+
"type": "function",
199+
"name": "setNumber",
200+
"inputs": [
201+
{
202+
"name": "newNumber",
203+
"type": "uint256",
204+
"internalType": "uint256"
205+
}
206+
],
207+
"outputs": [],
208+
"stateMutability": "nonpayable"
209+
}
210+
]
211+
164212
165213
"#]]);
166214

215+
// Dry-run with `--json` flag
216+
cmd.arg("--json").assert().stdout_eq(
217+
str![[r#"
218+
{
219+
"contract": "Counter",
220+
"transaction": {
221+
"from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
222+
"to": null,
223+
"maxFeePerGas": "0x77359401",
224+
"maxPriorityFeePerGas": "0x1",
225+
"gas": "0x17575",
226+
"input": "[..]",
227+
"nonce": "0x0",
228+
"chainId": "0x7a69"
229+
},
230+
"abi": [
231+
{
232+
"type": "function",
233+
"name": "increment",
234+
"inputs": [],
235+
"outputs": [],
236+
"stateMutability": "nonpayable"
237+
},
238+
{
239+
"type": "function",
240+
"name": "number",
241+
"inputs": [],
242+
"outputs": [
243+
{
244+
"name": "",
245+
"type": "uint256",
246+
"internalType": "uint256"
247+
}
248+
],
249+
"stateMutability": "view"
250+
},
251+
{
252+
"type": "function",
253+
"name": "setNumber",
254+
"inputs": [
255+
{
256+
"name": "newNumber",
257+
"type": "uint256",
258+
"internalType": "uint256"
259+
}
260+
],
261+
"outputs": [],
262+
"stateMutability": "nonpayable"
263+
}
264+
]
265+
}
266+
267+
"#]]
268+
.is_json(),
269+
);
270+
271+
cmd.forge_fuse().args([
272+
"create",
273+
format!("./src/{TEMPLATE_CONTRACT}.sol:{TEMPLATE_CONTRACT}").as_str(),
274+
"--rpc-url",
275+
rpc.as_str(),
276+
"--private-key",
277+
pk.as_str(),
278+
"--broadcast",
279+
]);
280+
167281
cmd.assert().stdout_eq(str![[r#"
168282
No files changed, compilation skipped
169283
Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
170-
Deployed to: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
284+
Deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
171285
[TX_HASH]
172286
173287
"#]]);
@@ -193,6 +307,7 @@ forgetest_async!(can_create_using_unlocked, |prj, cmd| {
193307
"--from",
194308
format!("{dev:?}").as_str(),
195309
"--unlocked",
310+
"--broadcast",
196311
]);
197312

198313
cmd.assert().stdout_eq(str![[r#"
@@ -204,6 +319,7 @@ Deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
204319
[TX_HASH]
205320
206321
"#]]);
322+
207323
cmd.assert().stdout_eq(str![[r#"
208324
No files changed, compilation skipped
209325
Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
@@ -248,6 +364,7 @@ contract ConstructorContract {
248364
rpc.as_str(),
249365
"--private-key",
250366
pk.as_str(),
367+
"--broadcast",
251368
"--constructor-args",
252369
"My Constructor",
253370
])
@@ -285,6 +402,7 @@ contract TupleArrayConstructorContract {
285402
rpc.as_str(),
286403
"--private-key",
287404
pk.as_str(),
405+
"--broadcast",
288406
"--constructor-args",
289407
"[(1,2), (2,3), (3,4)]",
290408
])
@@ -335,6 +453,7 @@ contract UniswapV2Swap {
335453
rpc.as_str(),
336454
"--private-key",
337455
pk.as_str(),
456+
"--broadcast",
338457
])
339458
.assert_success()
340459
.stdout_eq(str![[r#"

0 commit comments

Comments
 (0)