Skip to content

Commit e130b06

Browse files
authored
Support extra eip7702Auth parameter in eip4337 user operation (#4389)
See https://eips.ethereum.org/EIPS/eip-4337#support-for-eip-7702-authorizations
1 parent 302938c commit e130b06

File tree

5 files changed

+168
-5
lines changed

5 files changed

+168
-5
lines changed

rust/tw_evm/src/modules/tx_builder.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use crate::transaction::transaction_eip1559::TransactionEip1559;
2020
use crate::transaction::transaction_eip7702::TransactionEip7702;
2121
use crate::transaction::transaction_non_typed::TransactionNonTyped;
2222
use crate::transaction::user_operation::UserOperation;
23-
use crate::transaction::user_operation_v0_7::UserOperationV0_7;
23+
use crate::transaction::user_operation_v0_7::{Eip7702Auth, UserOperationV0_7};
2424
use crate::transaction::UnsignedTransactionBox;
2525
use std::marker::PhantomData;
2626
use std::str::FromStr;
@@ -528,6 +528,20 @@ impl<Context: EvmContext> TxBuilder<Context> {
528528
.into_tw()
529529
.context("Paymaster post-op gas limit exceeds u128")?;
530530

531+
let eip7702_auth =
532+
Self::build_authorization_list(input, sender)
533+
.ok()
534+
.and_then(|auth_list| {
535+
auth_list.0.first().map(|signed_auth| Eip7702Auth {
536+
chain_id: signed_auth.authorization.chain_id,
537+
address: signed_auth.authorization.address,
538+
nonce: signed_auth.authorization.nonce,
539+
y_parity: U256::from(signed_auth.y_parity),
540+
r: signed_auth.r,
541+
s: signed_auth.s,
542+
})
543+
});
544+
531545
let entry_point = Self::parse_address(user_op_v0_7.entry_point.as_ref())
532546
.context("Invalid entry point")?;
533547

@@ -546,6 +560,7 @@ impl<Context: EvmContext> TxBuilder<Context> {
546560
paymaster_verification_gas_limit,
547561
paymaster_post_op_gas_limit,
548562
paymaster_data: user_op_v0_7.paymaster_data.to_vec(),
563+
eip7702_auth,
549564
entry_point,
550565
})
551566
}

rust/tw_evm/src/transaction/authorization_list.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ impl RlpEncode for SignedAuthorization {
4848
}
4949
}
5050

51-
/// [EIP-2930](https://eips.ethereum.org/EIPS/eip-2930) access list.
51+
/// [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702) authorization list.
5252
#[derive(Default)]
53-
pub struct AuthorizationList(Vec<SignedAuthorization>);
53+
pub struct AuthorizationList(pub(crate) Vec<SignedAuthorization>);
5454

5555
impl From<Vec<SignedAuthorization>> for AuthorizationList {
5656
fn from(value: Vec<SignedAuthorization>) -> Self {

rust/tw_evm/src/transaction/user_operation_v0_7.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,30 @@ pub struct UserOperationV0_7 {
156156
#[serde(with = "as_hex_prefixed")]
157157
pub paymaster_data: Data,
158158

159+
#[serde(skip_serializing_if = "Option::is_none")]
160+
pub eip7702_auth: Option<Eip7702Auth>,
161+
159162
#[serde(skip)]
160163
pub entry_point: Address,
161164
}
162165

166+
// See: https://eips.ethereum.org/EIPS/eip-4337#support-for-eip-7702-authorizations
167+
#[derive(Serialize)]
168+
#[serde(rename_all = "camelCase")]
169+
pub struct Eip7702Auth {
170+
#[serde(serialize_with = "U256::as_hex")]
171+
pub chain_id: U256,
172+
pub address: Address,
173+
#[serde(serialize_with = "U256::as_hex")]
174+
pub nonce: U256,
175+
#[serde(serialize_with = "U256::as_hex")]
176+
pub y_parity: U256,
177+
#[serde(serialize_with = "U256::as_hex")]
178+
pub r: U256,
179+
#[serde(serialize_with = "U256::as_hex")]
180+
pub s: U256,
181+
}
182+
163183
impl TransactionCommon for UserOperationV0_7 {
164184
#[inline]
165185
fn payload(&self) -> Data {
@@ -277,6 +297,7 @@ mod tests {
277297
paymaster_verification_gas_limit: 0,
278298
paymaster_post_op_gas_limit: 0,
279299
paymaster_data: Vec::default(),
300+
eip7702_auth: None,
280301
entry_point,
281302
};
282303

@@ -325,6 +346,7 @@ mod tests {
325346
paymaster_verification_gas_limit: 99999u128,
326347
paymaster_post_op_gas_limit: 88888u128,
327348
paymaster_data: "00000000000b0000000000002e234dae75c793f67a35089c9d99245e1c58470b00000000000000000000000000000000000000000000000000000000000186a0072f35038bcacc31bcdeda87c1d9857703a26fb70a053f6e87da5a4e7a1e1f3c4b09fbe2dbff98e7a87ebb45a635234f4b79eff3225d07560039c7764291c97e1b".decode_hex().unwrap(),
349+
eip7702_auth: None,
328350
entry_point: Address::from("0x0000000071727De22E5E9d8BAf0edAc6f37da032"),
329351
};
330352
let packed_user_op = PackedUserOperation::new(&user_op);

rust/tw_tests/tests/chains/ethereum/ethereum_compile.rs

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,16 @@ use tw_any_coin::ffi::tw_transaction_compiler::{
99
};
1010
use tw_coin_entry::error::prelude::*;
1111
use tw_coin_registry::coin_type::CoinType;
12+
use tw_encoding::hex;
1213
use tw_encoding::hex::{DecodeHex, ToHex};
1314
use tw_memory::test_utils::tw_data_helper::TWDataHelper;
1415
use tw_memory::test_utils::tw_data_vector_helper::TWDataVectorHelper;
16+
use tw_misc::assert_eq_json;
1517
use tw_number::U256;
1618
use tw_proto::Ethereum::Proto;
17-
use tw_proto::Ethereum::Proto::{Authorization, AuthorizationCustomSignature, TransactionMode};
19+
use tw_proto::Ethereum::Proto::{
20+
Authorization, AuthorizationCustomSignature, TransactionMode, UserOperationV0_7,
21+
};
1822
use tw_proto::TxCompiler::Proto as CompilerProto;
1923
use tw_proto::{deserialize, serialize};
2024

@@ -161,6 +165,128 @@ fn test_transaction_compiler_eip7702() {
161165
assert_eq!(output.encoded.to_hex(), expected_encoded);
162166
}
163167

168+
#[test]
169+
fn test_transaction_compiler_user_op_v0_7_with_eip7702() {
170+
// Step 1: Prepare the input
171+
let contract_generic = Proto::mod_Transaction::ContractGeneric {
172+
amount: U256::encode_be_compact(0),
173+
// call function: execute(address target, uint256 value, bytes calldata data)
174+
data: hex::decode("0xb61d27f60000000000000000000000003a4a63b8763749c4cd30909becfd0e68364870a400000000000000000000000000000000000000000000000000000000000004d200000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000").unwrap().into(),
175+
};
176+
let input = Proto::SigningInput {
177+
tx_mode: TransactionMode::UserOp,
178+
nonce: U256::encode_be_compact(2),
179+
chain_id: U256::encode_be_compact(11155111),
180+
gas_limit: U256::from(600000u128).to_big_endian_compact().into(),
181+
max_inclusion_fee_per_gas: U256::from(100000000u128).to_big_endian_compact().into(),
182+
max_fee_per_gas: U256::from(939734933u128).to_big_endian_compact().into(),
183+
transaction: Some(Proto::Transaction {
184+
transaction_oneof: Proto::mod_Transaction::OneOftransaction_oneof::contract_generic(
185+
contract_generic,
186+
),
187+
}),
188+
user_operation_oneof:
189+
Proto::mod_SigningInput::OneOfuser_operation_oneof::user_operation_v0_7(
190+
UserOperationV0_7 {
191+
entry_point: "0x0000000071727De22E5E9d8BAf0edAc6f37da032".into(),
192+
factory: Default::default(),
193+
factory_data: "".decode_hex().unwrap().into(),
194+
sender: "0x18356de2Bc664e45dD22266A674906574087Cf54".into(),
195+
pre_verification_gas: U256::from(400000u64).to_big_endian_compact().into(),
196+
verification_gas_limit: U256::from(1000000u128).to_big_endian_compact().into(),
197+
paymaster: Cow::from(""),
198+
paymaster_verification_gas_limit: Vec::new().into(),
199+
paymaster_post_op_gas_limit: Vec::new().into(),
200+
paymaster_data: "".decode_hex().unwrap().into(),
201+
},
202+
),
203+
eip7702_authorization: Some(Authorization {
204+
address: "0xbA0db987220ecE42a60EE9f48FDE633E6c938482".into(),
205+
custom_signature: Some(AuthorizationCustomSignature {
206+
nonce: U256::encode_be_compact(19),
207+
chain_id: U256::encode_be_compact(11155111),
208+
signature: "7b2abdf6d874487751f3efd4d4eb8d39a50569c656f5863cf4de064caafa0e106954dfea7fa4e91e8d20c08e78b4cb2e21a7e56072b02d622fbb5637b5a0d9d701".into(),
209+
}),
210+
}),
211+
..Proto::SigningInput::default()
212+
};
213+
214+
// Step 2: Obtain preimage hash
215+
let input_data = TWDataHelper::create(serialize(&input).unwrap());
216+
let preimage_data = TWDataHelper::wrap(unsafe {
217+
tw_transaction_compiler_pre_image_hashes(CoinType::Ethereum as u32, input_data.ptr())
218+
})
219+
.to_vec()
220+
.expect("!tw_transaction_compiler_pre_image_hashes returned nullptr");
221+
222+
let preimage: CompilerProto::PreSigningOutput =
223+
deserialize(&preimage_data).expect("Coin entry returned an invalid output");
224+
assert_eq!(preimage.error, SigningErrorType::OK);
225+
assert!(preimage.error_message.is_empty());
226+
assert_eq!(
227+
preimage.data_hash.to_hex(),
228+
"3bf2bf012750e35a16e3430b010f2eb2dda8309892309afce6714196d4912d56" // The AA user op hash
229+
);
230+
231+
// Step 3: Compile transaction info
232+
// Simulate signature, normally obtained from signature server
233+
let signature = "4143bd278c97a4738846d9bc5756d0433bd062ea321cafb5ad3f7e3f68c68385277444f6fc851401ab319dad3756cd9e83c3ba9dfa502a69c43827fc2267f6d701".decode_hex().unwrap();
234+
let public_key = "04ec6632291fbfe6b47826a1c4b195f8b112a7e147e8a5a15fb0f7d7de022652e7f65b97a57011c09527688e23c07ae9c83a2cae2e49edba226e7c43f0baa7296d".decode_hex().unwrap();
235+
236+
let signatures = TWDataVectorHelper::create([signature]);
237+
let public_keys = TWDataVectorHelper::create([public_key]);
238+
239+
let input_data = TWDataHelper::create(serialize(&input).unwrap());
240+
let output_data = TWDataHelper::wrap(unsafe {
241+
tw_transaction_compiler_compile(
242+
CoinType::Ethereum as u32,
243+
input_data.ptr(),
244+
signatures.ptr(),
245+
public_keys.ptr(),
246+
)
247+
})
248+
.to_vec()
249+
.expect("!tw_transaction_compiler_compile returned nullptr");
250+
251+
let output: Proto::SigningOutput =
252+
deserialize(&output_data).expect("Coin entry returned an invalid output");
253+
254+
assert_eq!(output.error, SigningErrorType::OK);
255+
assert!(output.error_message.is_empty());
256+
257+
let expected_encoded = r#"
258+
{
259+
"sender": "0x18356de2Bc664e45dD22266A674906574087Cf54",
260+
"nonce": "0x02",
261+
"callData": "0xb61d27f60000000000000000000000003a4a63b8763749c4cd30909becfd0e68364870a400000000000000000000000000000000000000000000000000000000000004d200000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000",
262+
"callGasLimit": "0x0927c0",
263+
"verificationGasLimit": "0x0f4240",
264+
"preVerificationGas": "0x061a80",
265+
"maxFeePerGas": "0x38033795",
266+
"maxPriorityFeePerGas": "0x05f5e100",
267+
"paymasterVerificationGasLimit": "0x00",
268+
"paymasterPostOpGasLimit": "0x00",
269+
"eip7702Auth": {
270+
"chainId": "0xaa36a7",
271+
"address": "0xbA0db987220ecE42a60EE9f48FDE633E6c938482",
272+
"nonce": "0x13",
273+
"yParity": "0x01",
274+
"r": "0x7b2abdf6d874487751f3efd4d4eb8d39a50569c656f5863cf4de064caafa0e10",
275+
"s": "0x6954dfea7fa4e91e8d20c08e78b4cb2e21a7e56072b02d622fbb5637b5a0d9d7"
276+
},
277+
"signature": "0x4143bd278c97a4738846d9bc5756d0433bd062ea321cafb5ad3f7e3f68c68385277444f6fc851401ab319dad3756cd9e83c3ba9dfa502a69c43827fc2267f6d71c"
278+
}
279+
"#;
280+
// Successfully broadcast by a bundler:
281+
// https://sepolia.etherscan.io/tx/0x7b89079050a3f3f5afadaeda93750ed797132eb35e038962d15a18f9e862bb58
282+
// The AA Transaction:
283+
// https://sepolia.etherscan.io/tx/0x3bf2bf012750e35a16e3430b010f2eb2dda8309892309afce6714196d4912d56
284+
assert_eq_json!(
285+
std::str::from_utf8(output.encoded.iter().as_slice()).unwrap(),
286+
expected_encoded
287+
);
288+
}
289+
164290
#[test]
165291
fn test_transaction_compiler_plan_not_supported() {
166292
let transfer = Proto::mod_Transaction::Transfer {

src/proto/Ethereum.proto

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ message SigningInput {
274274
repeated Access access_list = 12;
275275

276276
// EIP7702 authorization.
277-
// Used in `TransactionMode::SetOp` only.
277+
// Used in `TransactionMode::SetOp` or `TransactionMode::UserOp`.
278278
// Currently, we support delegation to only one authority at a time.
279279
Authorization eip7702_authorization = 15;
280280
}

0 commit comments

Comments
 (0)