Skip to content

Commit 297d4e2

Browse files
feat(forge): cheatcodes to crosschain sign and attach delegation (#10518)
* feat(forge): cheatcodes to crosschain sign and attach delegation * Update crates/cheatcodes/spec/src/vm.rs Co-authored-by: zerosnacks <[email protected]> * Update crates/cheatcodes/spec/src/vm.rs Co-authored-by: zerosnacks <[email protected]> * Update crates/cheatcodes/spec/src/vm.rs Co-authored-by: zerosnacks <[email protected]> * Nits --------- Co-authored-by: zerosnacks <[email protected]>
1 parent b47cf78 commit 297d4e2

File tree

5 files changed

+179
-26
lines changed

5 files changed

+179
-26
lines changed

crates/cheatcodes/assets/cheatcodes.json

Lines changed: 61 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cheatcodes/spec/src/vm.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2229,10 +2229,18 @@ interface Vm {
22292229
#[cheatcode(group = Scripting)]
22302230
function signDelegation(address implementation, uint256 privateKey, uint64 nonce) external returns (SignedDelegation memory signedDelegation);
22312231

2232+
/// Sign an EIP-7702 authorization for delegation, with optional cross-chain validity.
2233+
#[cheatcode(group = Scripting)]
2234+
function signDelegation(address implementation, uint256 privateKey, bool crossChain) external returns (SignedDelegation memory signedDelegation);
2235+
22322236
/// Designate the next call as an EIP-7702 transaction
22332237
#[cheatcode(group = Scripting)]
22342238
function attachDelegation(SignedDelegation calldata signedDelegation) external;
22352239

2240+
/// Designate the next call as an EIP-7702 transaction, with optional cross-chain validity.
2241+
#[cheatcode(group = Scripting)]
2242+
function attachDelegation(SignedDelegation calldata signedDelegation, bool crossChain) external;
2243+
22362244
/// Sign an EIP-7702 authorization and designate the next call as an EIP-7702 transaction
22372245
#[cheatcode(group = Scripting)]
22382246
function signAndAttachDelegation(address implementation, uint256 privateKey) external returns (SignedDelegation memory signedDelegation);
@@ -2241,6 +2249,10 @@ interface Vm {
22412249
#[cheatcode(group = Scripting)]
22422250
function signAndAttachDelegation(address implementation, uint256 privateKey, uint64 nonce) external returns (SignedDelegation memory signedDelegation);
22432251

2252+
/// Sign an EIP-7702 authorization and designate the next call as an EIP-7702 transaction, with optional cross-chain validity.
2253+
#[cheatcode(group = Scripting)]
2254+
function signAndAttachDelegation(address implementation, uint256 privateKey, bool crossChain) external returns (SignedDelegation memory signedDelegation);
2255+
22442256
/// Attach an EIP-4844 blob to the next call
22452257
#[cheatcode(group = Scripting)]
22462258
function attachBlob(bytes calldata blob) external;

crates/cheatcodes/src/script.rs

Lines changed: 54 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -33,63 +33,93 @@ impl Cheatcode for broadcast_2Call {
3333
}
3434
}
3535

36-
impl Cheatcode for attachDelegationCall {
36+
impl Cheatcode for attachDelegation_0Call {
3737
fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
3838
let Self { signedDelegation } = self;
39-
let SignedDelegation { v, r, s, nonce, implementation } = signedDelegation;
39+
attach_delegation(ccx, signedDelegation, false)
40+
}
41+
}
4042

41-
let auth = Authorization {
42-
address: *implementation,
43-
nonce: *nonce,
44-
chain_id: U256::from(ccx.ecx.env.cfg.chain_id),
45-
};
46-
let signed_auth = SignedAuthorization::new_unchecked(
47-
auth,
48-
*v,
49-
U256::from_be_bytes(r.0),
50-
U256::from_be_bytes(s.0),
51-
);
52-
write_delegation(ccx, signed_auth.clone())?;
53-
ccx.state.active_delegation = Some(signed_auth);
54-
Ok(Default::default())
43+
impl Cheatcode for attachDelegation_1Call {
44+
fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
45+
let Self { signedDelegation, crossChain } = self;
46+
attach_delegation(ccx, signedDelegation, *crossChain)
5547
}
5648
}
5749

5850
impl Cheatcode for signDelegation_0Call {
5951
fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
6052
let Self { implementation, privateKey } = *self;
61-
sign_delegation(ccx, privateKey, implementation, None, false)
53+
sign_delegation(ccx, privateKey, implementation, None, false, false)
6254
}
6355
}
6456

6557
impl Cheatcode for signDelegation_1Call {
6658
fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
6759
let Self { implementation, privateKey, nonce } = *self;
68-
sign_delegation(ccx, privateKey, implementation, Some(nonce), false)
60+
sign_delegation(ccx, privateKey, implementation, Some(nonce), false, false)
61+
}
62+
}
63+
64+
impl Cheatcode for signDelegation_2Call {
65+
fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
66+
let Self { implementation, privateKey, crossChain } = *self;
67+
sign_delegation(ccx, privateKey, implementation, None, crossChain, false)
6968
}
7069
}
7170

7271
impl Cheatcode for signAndAttachDelegation_0Call {
7372
fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
7473
let Self { implementation, privateKey } = *self;
75-
sign_delegation(ccx, privateKey, implementation, None, true)
74+
sign_delegation(ccx, privateKey, implementation, None, false, true)
7675
}
7776
}
7877

7978
impl Cheatcode for signAndAttachDelegation_1Call {
8079
fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
8180
let Self { implementation, privateKey, nonce } = *self;
82-
sign_delegation(ccx, privateKey, implementation, Some(nonce), true)
81+
sign_delegation(ccx, privateKey, implementation, Some(nonce), false, true)
8382
}
8483
}
8584

85+
impl Cheatcode for signAndAttachDelegation_2Call {
86+
fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
87+
let Self { implementation, privateKey, crossChain } = *self;
88+
sign_delegation(ccx, privateKey, implementation, None, crossChain, true)
89+
}
90+
}
91+
92+
/// Helper function to attach an EIP-7702 delegation.
93+
fn attach_delegation(
94+
ccx: &mut CheatsCtxt,
95+
delegation: &SignedDelegation,
96+
cross_chain: bool,
97+
) -> Result {
98+
let SignedDelegation { v, r, s, nonce, implementation } = delegation;
99+
// Set chain id to 0 if universal deployment is preferred.
100+
// See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7702.md#protection-from-malleability-cross-chain
101+
let chain_id = if cross_chain { U256::from(0) } else { U256::from(ccx.ecx.env.cfg.chain_id) };
102+
103+
let auth = Authorization { address: *implementation, nonce: *nonce, chain_id };
104+
let signed_auth = SignedAuthorization::new_unchecked(
105+
auth,
106+
*v,
107+
U256::from_be_bytes(r.0),
108+
U256::from_be_bytes(s.0),
109+
);
110+
write_delegation(ccx, signed_auth.clone())?;
111+
ccx.state.active_delegation = Some(signed_auth);
112+
Ok(Default::default())
113+
}
114+
86115
/// Helper function to sign and attach (if needed) an EIP-7702 delegation.
87116
/// Uses the provided nonce, otherwise retrieves and increments the nonce of the EOA.
88117
fn sign_delegation(
89118
ccx: &mut CheatsCtxt,
90119
private_key: Uint<256, 4>,
91120
implementation: Address,
92121
nonce: Option<u64>,
122+
cross_chain: bool,
93123
attach: bool,
94124
) -> Result<Vec<u8>> {
95125
let signer = PrivateKeySigner::from_bytes(&B256::from(private_key))?;
@@ -101,11 +131,9 @@ fn sign_delegation(
101131
// If we don't have a nonce then use next auth account nonce.
102132
authority_acc.data.info.nonce + 1
103133
};
104-
let auth = Authorization {
105-
address: implementation,
106-
nonce,
107-
chain_id: U256::from(ccx.ecx.env.cfg.chain_id),
108-
};
134+
let chain_id = if cross_chain { U256::from(0) } else { U256::from(ccx.ecx.env.cfg.chain_id) };
135+
136+
let auth = Authorization { address: implementation, nonce, chain_id };
109137
let sig = signer.sign_hash_sync(&auth.signature_hash())?;
110138
// Attach delegation.
111139
if attach {
@@ -133,6 +161,7 @@ fn write_delegation(ccx: &mut CheatsCtxt, auth: SignedAuthorization) -> Result<(
133161

134162
if auth.address.is_zero() {
135163
// Set empty code if the delegation address of authority is 0x.
164+
// See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7702.md#behavior.
136165
ccx.ecx.journaled_state.set_code_with_hash(authority, Bytecode::default(), KECCAK_EMPTY);
137166
} else {
138167
let bytecode = Bytecode::new_eip7702(*auth.address());

testdata/cheats/Vm.sol

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

testdata/default/cheats/AttachDelegation.t.sol

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,22 @@ contract AttachDelegationTest is DSTest {
3939
assertEq(token.balanceOf(bob), 100);
4040
}
4141

42+
function testCallSingleAttachCrossChainDelegation() public {
43+
Vm.SignedDelegation memory signedDelegation = vm.signDelegation(address(implementation), alice_pk, true);
44+
SimpleDelegateContract.Call[] memory calls = new SimpleDelegateContract.Call[](1);
45+
bytes memory data = abi.encodeCall(ERC20.mint, (100, bob));
46+
calls[0] = SimpleDelegateContract.Call({to: address(token), data: data, value: 0});
47+
// executing as bob to make clear that we don't need to execute the tx as alice
48+
vm.broadcast(bob_pk);
49+
vm.attachDelegation(signedDelegation, true);
50+
51+
bytes memory code = address(alice).code;
52+
require(code.length > 0, "no code written to alice");
53+
SimpleDelegateContract(alice).execute(calls);
54+
55+
assertEq(token.balanceOf(bob), 100);
56+
}
57+
4258
/// forge-config: default.allow_internal_expect_revert = true
4359
function testCallSingleAttachDelegationWithNonce() public {
4460
Vm.SignedDelegation memory signedDelegation = vm.signDelegation(address(implementation), alice_pk, 11);
@@ -70,6 +86,26 @@ contract AttachDelegationTest is DSTest {
7086
assertEq(token.balanceOf(address(this)), 50);
7187
}
7288

89+
function testMultiCallAttachCrossChainDelegation() public {
90+
Vm.SignedDelegation memory signedDelegation = vm.signDelegation(address(implementation), alice_pk, true);
91+
vm.broadcast(bob_pk);
92+
vm.attachDelegation(signedDelegation, true);
93+
94+
SimpleDelegateContract.Call[] memory calls = new SimpleDelegateContract.Call[](2);
95+
calls[0] =
96+
SimpleDelegateContract.Call({to: address(token), data: abi.encodeCall(ERC20.mint, (50, bob)), value: 0});
97+
calls[1] = SimpleDelegateContract.Call({
98+
to: address(token),
99+
data: abi.encodeCall(ERC20.mint, (50, address(this))),
100+
value: 0
101+
});
102+
103+
SimpleDelegateContract(alice).execute(calls);
104+
105+
assertEq(token.balanceOf(bob), 50);
106+
assertEq(token.balanceOf(address(this)), 50);
107+
}
108+
73109
function testSwitchAttachDelegation() public {
74110
Vm.SignedDelegation memory signedDelegation = vm.signDelegation(address(implementation), alice_pk);
75111

@@ -138,6 +174,19 @@ contract AttachDelegationTest is DSTest {
138174
assertEq(token.balanceOf(bob), 100);
139175
}
140176

177+
function testCallSingleSignAndAttachCrossChainDelegation() public {
178+
SimpleDelegateContract.Call[] memory calls = new SimpleDelegateContract.Call[](1);
179+
bytes memory data = abi.encodeCall(ERC20.mint, (100, bob));
180+
calls[0] = SimpleDelegateContract.Call({to: address(token), data: data, value: 0});
181+
vm.signAndAttachDelegation(address(implementation), alice_pk, true);
182+
bytes memory code = address(alice).code;
183+
require(code.length > 0, "no code written to alice");
184+
vm.broadcast(bob_pk);
185+
SimpleDelegateContract(alice).execute(calls);
186+
187+
assertEq(token.balanceOf(bob), 100);
188+
}
189+
141190
/// forge-config: default.allow_internal_expect_revert = true
142191
function testCallSingleSignAndAttachDelegationWithNonce() public {
143192
vm._expectCheatcodeRevert("vm.signAndAttachDelegation: invalid nonce");

0 commit comments

Comments
 (0)