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

Support EIP-7702 Delegations in Forge #9236

Merged
merged 57 commits into from
Nov 20, 2024

Conversation

evchip
Copy link
Contributor

@evchip evchip commented Oct 31, 2024

Motivation

Adds initial support for EIP-7702 transaction delegation in Forge scripts.

Addresses #7128

Solution

  • adds EIP-7702 support to forge scripts for transaction batching + delegation.
  • new cheatcodes:

createDelegation: generates auth hash
signDelegation: signs auth with pk
attachDelegation: connects signed delegation to sender

Limitations:

  • clunky sig handling - requires passing v,r,s separately bc passing raw sig bytes breaks tests
  • only supports raw private keys, needs hardware wallet etc support

Questions:

  1. best way to propagate delegation state from cheatcodes -> broadcast?
  2. should we track delegations in ScriptSequence instead of ScriptResult?

looking for feedback on state handling and current api limitations before expanding functionality.

bytes32 delegation = vm.createDelegation(implementation, nonce);
(uint8 v, bytes32 r, bytes32 s) = vm.signDelegation(delegation, pk);

vm.attachDelegation(implementation, nonce, v, r, s);
Copy link
Member

Choose a reason for hiding this comment

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

I believe delegations would be attached to separate transactions vs broadcast blocks, so it would be something like

vm.startBroadcast();
vm.attachDelegation(implementation, nonce, v, r, s);
// this transactions would be broadcasted along with attached 7702 delegation
token.transfer(....);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

thanks for the comment! looking at this closer - since 7702 makes the EOA itself the target of the tx, i think we need to route all calls through the EOA first.

right now we can do this with a raw call:

vm.startBroadcast();
vm.attachDelegation(implementation, nonce, v, r, s);
alice.call(abi.encodeCall(implementation.execute, (calls)));

but for better UX, maybe we could either:

  1. change attachDelegation to take authority instead of implementation (since we have the implementation in the call itself):
vm.attachDelegation(alice, nonce, v, r, s);
implementation.execute(calls);
  1. or add a helper like:
vm.delegatedCall(alice, implementation.execute, calls);

what do you think would be more ergonomic for users?

@klkvr
Copy link
Member

klkvr commented Nov 1, 2024

best way to propagate delegation state from cheatcodes -> broadcast?

We should attach them to TransactionRequest here:

self.broadcastable_transactions.push_back(BroadcastableTransaction {
rpc: ecx.db.active_fork_url(),
transaction: TransactionRequest {
from: Some(broadcast.new_origin),
to: Some(TxKind::from(Some(call.target_address))),
value: call.transfer_value(),
input: TransactionInput::new(call.input.clone()),
nonce: Some(account.info.nonce),
gas: if is_fixed_gas_limit { Some(call.gas_limit) } else { None },
..Default::default()
}
.into(),
});

should we track delegations in ScriptSequence instead of ScriptResult?

I think we need to track them in both. In ScriptResult as part of BroadcastableTransactions and in ScriptSequence as TransactionWithMetadata

@evchip
Copy link
Contributor Author

evchip commented Nov 1, 2024

@klkvr thanks for the detailed feedback! Will push an update soon that addresses your comments.

@evchip
Copy link
Contributor Author

evchip commented Nov 3, 2024

@klkvr thanks for the pointer on TransactionRequest. i've added the authorization_list field and i'm constructing 7702 txs (type=0x04) when that field is populated. however, the delegated calls don't seem to be executing properly - i suspect we also need to write the delegation designation (0xef0100 || address) to the EOA's code somewhere.

looking at the anvil tests, it seems revm supports 7702, but i'm not clear on where in foundry we should be handling the delegation setup. any guidance? thanks in advance.

@klkvr
Copy link
Member

klkvr commented Nov 3, 2024

@klkvr thanks for the pointer on TransactionRequest. i've added the authorization_list field and i'm constructing 7702 txs (type=0x04) when that field is populated. however, the delegated calls don't seem to be executing properly - i suspect we also need to write the delegation designation (0xef0100 || address) to the EOA's code somewhere.

looking at the anvil tests, it seems revm supports 7702, but i'm not clear on where in foundry we should be handling the delegation setup. any guidance? thanks in advance.

right, we need to execute something similar to this https://github.com/bluealloy/revm/blob/346aa01398aa9b4e4e36776c21f6fe7169ed5d87/crates/revm/src/handler/mainnet/pre_execution.rs#L102 in call hook where we push to broadcastable_transactions

we can probably skip some of the steps as we will anyway execute this later during on-chain simulation, but I think nonce check and increase should be kept because those may affect contract deployment addresses

also we'll need to respect the authorization list here

let result = runner
when configuring TxEnv for simulation, this might require small changes to executor

…rom signDelegation, as it can be constructed in cheatcode.
…g signature and setting bytecode on EOA; refactor signDelegation cheatcode implementation to get nonce from signer
…alice account through SimpleDelegateContract
…ation - previous approach kept the delegation in the TxEnv, resulting in higher gas cost for all subsequent calls after the delegation was applied
Comment on lines 35 to 37
impl Cheatcode for attachDelegationCall {
fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
let Self { implementation, authority, v, r, s } = self;
Copy link
Member

Choose a reason for hiding this comment

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

let's remove authority argument from here. it's redundant as we are always recovering it from signature

also wdyt on adding signAndAttach method? I think common flow would be calling both of the cheatcodes

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'm not seeing how to construct the authorization without the authority's nonce, which we'd need to recover the authority from the signature. Can you clarify the flow you're thinking of?

Copy link
Member

Choose a reason for hiding this comment

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

we can get the nonce from state

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, but to get the nonce from state we need the authority's address to load their account - am I missing some other way to get their nonce?

Copy link
Member

Choose a reason for hiding this comment

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

in signDelegation we can get authority address from private key, then get nonce, construct and sign delegation

and in attachDelegation we can just recover authority from the signed delegation. right now the passed authority address is anyway expected to be the same as recovered one making it redundant

Copy link
Member

Choose a reason for hiding this comment

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

ah I see what you mean. didn't notice that attachDelegation doesn't accept nonce

let's make signDelegation return a new struct SignedDelegation which will contain v,r,s,nonce,implementation. And then attachDelegation would just accept this object

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've implemented a new cheatcode signAndAttachDelegation and refactored the existing cheatcodes to use struct SignedDelegation. Let me know what you think. Thanks!

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, lgtm!

@klkvr klkvr marked this pull request as ready for review November 18, 2024 14:38
@zerosnacks zerosnacks self-requested a review November 19, 2024 10:41
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.

Thanks @evchip! Implementation looks good to me

Copy link
Member

@yash-atreya yash-atreya left a comment

Choose a reason for hiding this comment

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

Can be merged @grandizzy

@grandizzy grandizzy enabled auto-merge (squash) November 20, 2024 11:36
@grandizzy grandizzy merged commit 2a194bd into foundry-rs:master Nov 20, 2024
22 checks passed
mds1 pushed a commit to foundry-rs/forge-std that referenced this pull request Nov 25, 2024
- add `delegatecall` flag to `prank` cheatcodes
(foundry-rs/foundry#8863)
- support EIP-7702 Delegations (`create/sign/attachDelegation`)
(foundry-rs/foundry#9236)
- add `contains` to check if a string contains another string
(foundry-rs/foundry#9085)
rplusq pushed a commit to rplusq/foundry that referenced this pull request Nov 29, 2024
* add EIP-7702 cheatcodes: createDelegation, signDelegation, attachDelegation

* add cheatcode implementations for EIP-7702: createDelegationCall, signDelegationCall, attachDelegationCall; modify broadcast to check if sender has a delegation

* add delegations hashmap to Cheatcodes struct

* add revm crate

* create AttachDelegationTest for EIP-7702 transactions

* regen cheatcodes.json

* cargo fmt

* move broadcast under attachDelegation

* combine createDelegationCall logic with signDelegationCall in order to create and sign delegation in a single call; remove delegation logic from broadcast() - no need to track delegations here

* remove revm import from workspace

* combine createDelegation logic inton signDelegation for simplicity

* remove revm from forge script deps

* combine createDelegation with signDelegation

* WIP - refactor test to use SimpleDelegateContract and ERC20 - test currently failing bc 7702 implementation.execute not executed as Alice EOA

* add logic to include authorization_list for EIP 7702 in TransactionRequest by searching delegations hash map by active_delegation

* add address authority param to attachDelegation; remove nonce param from signDelegation, as it can be constructed in cheatcode.

* remove 7702 tx request construction logic - now handled in attachDelegation cheatcode implementation

* refactor attachDelegation cheatcode implementation to handle verifying signature and setting bytecode on EOA; refactor signDelegation cheatcode implementation to get nonce from signer

* remove nonce param from attachDelegation cheatcode in favor of loading from authority account

* refactor test to check for code on alice account and call execute on alice account through SimpleDelegateContract

* revert refactor on TransactionRequest

* format

* cargo fmt

* fix clippy errors

* remove faulty logic comparing nonce to itself - nonce still checked by recovered signature

* add more tests to cover revert cases on attachDelegation and multiple calls via delegation contract

* cargo fmt

* restore logic to check if there's an active delegation when building TransactionRequest; add fixed values for gas and max_priority_fee_per_gas to ensure tx success, with TODO comment to explain what's left

* remove obsolete comment

* add comments explaining delegations and active_delegation

* cargo fmt

* add logic to increase gas limit by PER_EMPTY_ACCOUNT_COST(25k) if tx includes authorization list for EIP 7702 tx, which is seemingly not accounted for in gas estimation; remove hardcoded gas values from call_with_executor

* revert logic to add PER_EMPTY_ACCOUNT_COST for EIP 7702 txs - handled inside of revm now

* remove manually setting transaction type to 4 if auth list is present - handled in revm

* add method set_delegation to Executor for setting EIP-7702 authorization list in the transaction environment; call set_delegation from simulate_and_fill if auth list is not empty

* remove redundancy with TransactionMaybeSigned var tx

* cargo fmt

* refactor: use authorization_list() helper to return authorization_list and set delegation

* refactor: change Cheatcodes::active_delegation to Option<SignedAuthorization> and remove delegations hashmap - tx will only use one active delegation at a time, so no need for mapping

* replace verbose logic to set bytecode on EOA with journaled_state.set_code helper

* cargo fmt

* increment nonce of authority account

* add logic to set authorization_list to None if active_delegation is None

* add test testSwitchDelegation to assert that attaching an additional delegation switches the implementation on the EOA

* remove set_delegation logic in favor of adding call_raw_with_authorization - previous approach kept the delegation in the TxEnv, resulting in higher gas cost for all subsequent calls after the delegation was applied

* refactor signDelegation to return struct SignedDelegation and for attachDelegation to accept SignedDelegation

* update delegation tests to reflect change in cheatcode interface for signDelegation and attachDelegation

* add cheatcode signAndAttachDelegation

* add signAndAttachDelegationCall cheatcode logic; refactor helper methods for shared logic used in 7702 delegation cheatcodes

* add test testCallSingleSignAndAttachDelegation for new cheatcode signAndAttachDelegation

* add comments to SignedDelegation struct and cargo fmt

* cargo fmt

* fix ci

* fix spec

---------

Co-authored-by: Arsenii Kulikov <klkvrr@gmail.com>
Co-authored-by: grandizzy <38490174+grandizzy@users.noreply.github.com>
@grandizzy grandizzy added T-feature Type: feature C-forge Command: forge labels Dec 18, 2024
thomas-lamb-tech pushed a commit to thomas-lamb-tech/std_forge that referenced this pull request Dec 26, 2024
- add `delegatecall` flag to `prank` cheatcodes
(foundry-rs/foundry#8863)
- support EIP-7702 Delegations (`create/sign/attachDelegation`)
(foundry-rs/foundry#9236)
- add `contains` to check if a string contains another string
(foundry-rs/foundry#9085)
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
None yet
Development

Successfully merging this pull request may close these issues.

5 participants