Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(script): support custom create2 deployer #9278

Merged
merged 43 commits into from
Nov 30, 2024

Conversation

jsvisa
Copy link
Contributor

@jsvisa jsvisa commented Nov 7, 2024

Motivation

Currently when deploy contracts using CREATE2 method, it is default to

pub const DEFAULT_CREATE2_DEPLOYER: Address = address!("4e59b44847b379578588920ca78fbf26c0b4956c");

But in some circumstances, we can't access or deploy the 0x4e59b44847b379578588920ca78fbf26c0b4956c contract, may be caused of not able to send the pre EIP155 raw transaction. So try to introduce a env to change the default create2 deployer.

Solution

  1. add --create2-deployer to the forge script argument
  2. add create2_deployer to the foundry.toml config file

@grandizzy
Copy link
Collaborator

@jsvisa thank you! there's #2638 I think your PR could be closing it, can you pls confirm?

@jsvisa
Copy link
Contributor Author

jsvisa commented Nov 7, 2024

@jsvisa thank you! there's #2638 I think your PR could be closing it, can you pls confirm?

I think we can close the later one 😀

let user define create2 factory

@grandizzy
Copy link
Collaborator

I think we can close the later one 😀

Makes sense, waiting for a follow up project to close it then 😀

@jsvisa
Copy link
Contributor Author

jsvisa commented Nov 8, 2024

Makes sense, waiting for a follow up project to close it then 😀

Haha, so how about this one, is there anything I need to adjust

Copy link
Collaborator

@grandizzy grandizzy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I see the PR is only for script, but it won't be applied in other scopes, as for labels in traces, contract verification, (default) computeCreate2Address cheatcode and other places, which I think could lead to inconsistencies in deployment process? I'd say a consistent experience would be preferred here, that is when setting the address as env var then to be reflected in all places.

crates/evm/core/src/constants.rs Outdated Show resolved Hide resolved
@jsvisa
Copy link
Contributor Author

jsvisa commented Nov 8, 2024

@grandizzy thanks for the advice, I use forge to deploy contracts in a private evm chain, so only tested the script yet. Thanks for the scope you mentioned, I'll implement them later(maybe after the Devcon) :)

And If the changes are large, I'd prefer to split them into small PRs to include them all, else just in this one PR, WDYT?

@grandizzy
Copy link
Collaborator

@grandizzy thanks for the advice, I use forge to deploy contracts in a private evm chain, so only tested the script yet. Thanks for the scope you mentioned, I'll implement them later(maybe after the Devcon) :)

And If the changes are large, I'd prefer to split them into small PRs to include them all, else just in this one PR, WDYT?

ah, I see, then maybe should be done as a new --create2-address script arg instead env var... @zerosnacks @mattsse @yash-atreya could you please share your thoughts on this approach? thank you!

@jsvisa
Copy link
Contributor Author

jsvisa commented Nov 8, 2024

ah, I see, then maybe should be done as a new --create2-address script arg instead env var

I've tried this approach, but failed, we need to register the create2 handler here

pub fn create2_handler_register<I: InspectorExt>(
handler: &mut EvmHandler<'_, I, &mut dyn DatabaseExt>,
) {
let create2_overrides = Rc::<RefCell<Vec<_>>>::new(RefCell::new(Vec::new()));
let create2_overrides_inner = create2_overrides.clone();
let old_handle = handler.execution.create.clone();
handler.execution.create =
Arc::new(move |ctx, mut inputs| -> Result<FrameOrResult, EVMError<DatabaseError>> {
let CreateScheme::Create2 { salt } = inputs.scheme else {
return old_handle(ctx, inputs);
};
if !ctx.external.should_use_create2_factory(&mut ctx.evm, &mut inputs) {
return old_handle(ctx, inputs);
}
let gas_limit = inputs.gas_limit;
// Get CREATE2 deployer.
let create2_deployer = get_create2_deployer();
// Generate call inputs for CREATE2 factory.
let mut call_inputs = get_create2_factory_call_inputs(salt, *inputs, create2_deployer);
// Call inspector to change input or return outcome.
let outcome = ctx.external.call(&mut ctx.evm, &mut call_inputs);
// Push data about current override to the stack.
create2_overrides_inner
.borrow_mut()
.push((ctx.evm.journaled_state.depth(), call_inputs.clone()));
// Sanity check that CREATE2 deployer exists.
let code_hash = ctx.evm.load_account(create2_deployer)?.info.code_hash;
if code_hash == KECCAK_EMPTY {

seems it's hard to pass the cli argument into create2 schema, else maybe we need to adjust it in revm's codebase.

@klkvr
Copy link
Member

klkvr commented Nov 8, 2024

ah, I see, then maybe should be done as a new --create2-address script arg instead env var

I've tried this approach, but failed, we need to register the create2 handler here

pub fn create2_handler_register<I: InspectorExt>(
handler: &mut EvmHandler<'_, I, &mut dyn DatabaseExt>,
) {
let create2_overrides = Rc::<RefCell<Vec<_>>>::new(RefCell::new(Vec::new()));
let create2_overrides_inner = create2_overrides.clone();
let old_handle = handler.execution.create.clone();
handler.execution.create =
Arc::new(move |ctx, mut inputs| -> Result<FrameOrResult, EVMError<DatabaseError>> {
let CreateScheme::Create2 { salt } = inputs.scheme else {
return old_handle(ctx, inputs);
};
if !ctx.external.should_use_create2_factory(&mut ctx.evm, &mut inputs) {
return old_handle(ctx, inputs);
}
let gas_limit = inputs.gas_limit;
// Get CREATE2 deployer.
let create2_deployer = get_create2_deployer();
// Generate call inputs for CREATE2 factory.
let mut call_inputs = get_create2_factory_call_inputs(salt, *inputs, create2_deployer);
// Call inspector to change input or return outcome.
let outcome = ctx.external.call(&mut ctx.evm, &mut call_inputs);
// Push data about current override to the stack.
create2_overrides_inner
.borrow_mut()
.push((ctx.evm.journaled_state.depth(), call_inputs.clone()));
// Sanity check that CREATE2 deployer exists.
let code_hash = ctx.evm.load_account(create2_deployer)?.info.code_hash;
if code_hash == KECCAK_EMPTY {

seems it's hard to pass the cli argument into create2 schema, else maybe we need to adjust it in revm's codebase.

we can add a helper to InspectorExt to get the deployer address and then just configureInspectorStack with it

@jsvisa
Copy link
Contributor Author

jsvisa commented Nov 8, 2024

ah, I see, then maybe should be done as a new --create2-address script arg instead env var

I've tried this approach, but failed, we need to register the create2 handler here

pub fn create2_handler_register<I: InspectorExt>(
handler: &mut EvmHandler<'_, I, &mut dyn DatabaseExt>,
) {
let create2_overrides = Rc::<RefCell<Vec<_>>>::new(RefCell::new(Vec::new()));
let create2_overrides_inner = create2_overrides.clone();
let old_handle = handler.execution.create.clone();
handler.execution.create =
Arc::new(move |ctx, mut inputs| -> Result<FrameOrResult, EVMError<DatabaseError>> {
let CreateScheme::Create2 { salt } = inputs.scheme else {
return old_handle(ctx, inputs);
};
if !ctx.external.should_use_create2_factory(&mut ctx.evm, &mut inputs) {
return old_handle(ctx, inputs);
}
let gas_limit = inputs.gas_limit;
// Get CREATE2 deployer.
let create2_deployer = get_create2_deployer();
// Generate call inputs for CREATE2 factory.
let mut call_inputs = get_create2_factory_call_inputs(salt, *inputs, create2_deployer);
// Call inspector to change input or return outcome.
let outcome = ctx.external.call(&mut ctx.evm, &mut call_inputs);
// Push data about current override to the stack.
create2_overrides_inner
.borrow_mut()
.push((ctx.evm.journaled_state.depth(), call_inputs.clone()));
// Sanity check that CREATE2 deployer exists.
let code_hash = ctx.evm.load_account(create2_deployer)?.info.code_hash;
if code_hash == KECCAK_EMPTY {

seems it's hard to pass the cli argument into create2 schema, else maybe we need to adjust it in revm's codebase.

we can add a helper to InspectorExt to get the deployer address and then just configureInspectorStack with it

Thanks for the advice, I'll have a try, cli argument is much more implicit than environment.

@jsvisa jsvisa force-pushed the script-deployer branch 2 times, most recently from 918f485 to 109a713 Compare November 10, 2024 00:38
@jsvisa jsvisa force-pushed the script-deployer branch 3 times, most recently from abbb7b1 to b5928e4 Compare November 21, 2024 02:26
@jsvisa
Copy link
Contributor Author

jsvisa commented Nov 21, 2024

@grandizzy @klkvr sorry for the long delay, I think it's time to review.

@grandizzy grandizzy self-requested a review November 27, 2024 12:36
Copy link
Collaborator

@grandizzy grandizzy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you, looks good! pending other reviews before merging

Copy link
Member

@zerosnacks zerosnacks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lgtm! Thanks @jsvisa

Copy link
Member

@klkvr klkvr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice, this is what we want, left some nits

crates/script/src/lib.rs Outdated Show resolved Hide resolved
crates/script/src/lib.rs Outdated Show resolved Hide resolved
crates/script/src/runner.rs Outdated Show resolved Hide resolved
Comment on lines 871 to 880
tester
.add_deployer(0)
.load_private_keys(&[0])
.await
.add_create2_deployer(create2)
.add_sig("BroadcastTestNoLinking", "deployCreate2()")
.simulate(ScriptOutcome::OkSimulation)
.broadcast(ScriptOutcome::OkBroadcast)
.assert_nonce_increment(&[(0, 2)])
.await;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we assert here that it was deployed to the right address? either need a snapbox fixture or a cast.get_code check

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds reasonable

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should also add a test to check the create2's code is the same as the default one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still working on the deployed address test

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@klkvr sorry, can't think of a good way to test it with deployed contrat, so I test it by compare the contract address directly in the script in

function deployCreate2(address deployer) public {
vm.startBroadcast();
bytes32 salt = bytes32(uint256(1338));
NoLink test_c2 = new NoLink{salt: salt}();
assert(test_c2.view_me() == 1337);
address expectedAddress = address(
uint160(
uint256(
keccak256(
abi.encodePacked(
bytes1(0xff),
deployer,
salt,
keccak256(abi.encodePacked(type(NoLink).creationCode, abi.encode()))
)
)
)
)
);
require(address(test_c2) == expectedAddress, "Create2 address mismatch");

Copy link
Member

@klkvr klkvr Nov 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could use anvil's handle api https://github.com/jsvisa/foundry/blob/64545738dd183c64bd8d7a4ec94f7ba03c8d8d05/crates/forge/tests/cli/script.rs#L885

and then just do something like `assert!(!api.get_code(...).is_empty());

but it's fine anyway :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a quick question, if we use anvil to get the contract code, but how could we get the contract address? I was thought of calculate the address by

  1. compiling the solidity to get the init code
  2. get the initcodehash + salt + deployer address

but stuck in the first step 😭

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess you could use the anvil api to get the receipt and contract address from there, smth like we're getting the hash of last tx here

let tx_hash = api

Signed-off-by: jsvisa <[email protected]>

unit test

Signed-off-by: jsvisa <[email protected]>
Signed-off-by: jsvisa <[email protected]>
Signed-off-by: jsvisa <[email protected]>
Signed-off-by: jsvisa <[email protected]>
crates/script/src/lib.rs Outdated Show resolved Hide resolved
@klkvr klkvr enabled auto-merge (squash) November 29, 2024 23:56
@klkvr klkvr merged commit 7f41280 into foundry-rs:master Nov 30, 2024
21 checks passed
@jsvisa jsvisa deleted the script-deployer branch November 30, 2024 00:59
@grandizzy grandizzy added T-feature Type: feature C-forge Command: forge labels Dec 18, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-forge Command: forge T-feature Type: feature
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

4 participants