Skip to main content

chains_sdk/ethereum/
userop.rs

1//! ERC-4337 Account Abstraction — UserOperation builder and helpers.
2//!
3//! Implements the ERC-4337 UserOperation struct with ABI encoding,
4//! `userOpHash` computation, and callData builders for smart account
5//! `execute()` / `executeBatch()`.
6//!
7//! Supports both v0.6 (unpacked) and v0.7 (packed) formats.
8//!
9//! # Example
10//! ```no_run
11//! use chains_sdk::ethereum::userop::*;
12//!
13//! let mut op = UserOperation::new([0xAA; 20]);
14//! op.nonce = uint256_from_u64(1);
15//! op.call_data = encode_execute(&[0xBB; 20], uint256_from_u64(0), &[]);
16//! let hash = op.hash(&ENTRY_POINT_V06, uint256_from_u64(1));
17//! ```
18
19use crate::ethereum::abi::{self, AbiValue};
20use crate::ethereum::keccak256;
21
22/// A raw uint256 value encoded as 32-byte big-endian.
23pub type Uint256 = [u8; 32];
24
25// ═══════════════════════════════════════════════════════════════════
26// Constants
27// ═══════════════════════════════════════════════════════════════════
28
29/// EntryPoint v0.6 address (ERC-4337).
30pub const ENTRY_POINT_V06: [u8; 20] = [
31    0x5f, 0xf1, 0x37, 0xd4, 0xb0, 0xfd, 0xcd, 0x49, 0xdc, 0xa3, 0x0c, 0x7c, 0xf5, 0x7e, 0x57, 0x8a,
32    0x02, 0x6d, 0x27, 0x89,
33];
34
35/// EntryPoint v0.7 address.
36pub const ENTRY_POINT_V07: [u8; 20] = [
37    0x00, 0x00, 0x00, 0x00, 0x71, 0x72, 0x7d, 0xe2, 0x2e, 0x5e, 0x9d, 0x8b, 0xaf, 0x0e, 0xda, 0xc6,
38    0xf3, 0x7d, 0xa0, 0x32,
39];
40
41// ═══════════════════════════════════════════════════════════════════
42// UserOperation (v0.6)
43// ═══════════════════════════════════════════════════════════════════
44
45/// An ERC-4337 UserOperation (v0.6 format).
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct UserOperation {
48    /// Smart account address.
49    pub sender: [u8; 20],
50    /// Anti-replay nonce (key + sequence) as uint256.
51    pub nonce: Uint256,
52    /// Factory + factoryData for account creation (empty if account exists).
53    pub init_code: Vec<u8>,
54    /// Encoded function call on the smart account.
55    pub call_data: Vec<u8>,
56    /// Gas limit for the execution phase (uint256).
57    pub call_gas_limit: Uint256,
58    /// Gas for validation (validateUserOp + validatePaymasterUserOp) (uint256).
59    pub verification_gas_limit: Uint256,
60    /// Gas paid for the bundle tx overhead (uint256).
61    pub pre_verification_gas: Uint256,
62    /// Maximum fee per gas (EIP-1559) (uint256).
63    pub max_fee_per_gas: Uint256,
64    /// Maximum priority fee per gas (uint256).
65    pub max_priority_fee_per_gas: Uint256,
66    /// Paymaster address + data (empty if self-paying).
67    pub paymaster_and_data: Vec<u8>,
68    /// Signature over the UserOp hash.
69    pub signature: Vec<u8>,
70}
71
72impl UserOperation {
73    /// Create a new UserOperation with default gas values.
74    #[must_use]
75    pub fn new(sender: [u8; 20]) -> Self {
76        Self {
77            sender,
78            nonce: uint256_from_u64(0),
79            init_code: Vec::new(),
80            call_data: Vec::new(),
81            call_gas_limit: uint256_from_u64(100_000),
82            verification_gas_limit: uint256_from_u64(100_000),
83            pre_verification_gas: uint256_from_u64(21_000),
84            max_fee_per_gas: uint256_from_u64(1_000_000_000), // 1 gwei
85            max_priority_fee_per_gas: uint256_from_u64(1_000_000_000),
86            paymaster_and_data: Vec::new(),
87            signature: Vec::new(),
88        }
89    }
90
91    /// Compute the `userOpHash` for signing.
92    ///
93    /// `keccak256(abi.encode(pack(userOp), entryPoint, chainId))`
94    ///
95    /// This is the hash that the account validates in `validateUserOp`.
96    #[must_use]
97    pub fn hash(&self, entry_point: &[u8; 20], chain_id: Uint256) -> [u8; 32] {
98        // Step 1: Pack the UserOp (hash dynamic fields)
99        let packed_hash = self.pack_hash();
100
101        // Step 2: Encode (packedHash, entryPoint, chainId)
102        let mut entry_point_padded = [0u8; 32];
103        entry_point_padded[12..32].copy_from_slice(entry_point);
104
105        let encoded = abi::encode(&[
106            AbiValue::Uint256(packed_hash),
107            AbiValue::Uint256(entry_point_padded),
108            AbiValue::Uint256(chain_id),
109        ]);
110
111        keccak256(&encoded)
112    }
113
114    /// Compute the pack hash: hash of the UserOp with dynamic fields hashed.
115    fn pack_hash(&self) -> [u8; 32] {
116        let mut sender_padded = [0u8; 32];
117        sender_padded[12..32].copy_from_slice(&self.sender);
118
119        let values = vec![
120            AbiValue::Uint256(sender_padded),
121            AbiValue::Uint256(self.nonce),
122            AbiValue::Uint256(keccak256(&self.init_code)),
123            AbiValue::Uint256(keccak256(&self.call_data)),
124            AbiValue::Uint256(self.call_gas_limit),
125            AbiValue::Uint256(self.verification_gas_limit),
126            AbiValue::Uint256(self.pre_verification_gas),
127            AbiValue::Uint256(self.max_fee_per_gas),
128            AbiValue::Uint256(self.max_priority_fee_per_gas),
129            AbiValue::Uint256(keccak256(&self.paymaster_and_data)),
130        ];
131
132        keccak256(&abi::encode(&values))
133    }
134
135    /// ABI-encode the full UserOperation for bundler submission.
136    ///
137    /// Encodes as a tuple matching the Solidity struct layout.
138    #[must_use]
139    pub fn encode(&self) -> Vec<u8> {
140        let mut sender_padded = [0u8; 32];
141        sender_padded[12..32].copy_from_slice(&self.sender);
142
143        abi::encode(&[
144            AbiValue::Uint256(sender_padded),
145            AbiValue::Uint256(self.nonce),
146            AbiValue::Bytes(self.init_code.clone()),
147            AbiValue::Bytes(self.call_data.clone()),
148            AbiValue::Uint256(self.call_gas_limit),
149            AbiValue::Uint256(self.verification_gas_limit),
150            AbiValue::Uint256(self.pre_verification_gas),
151            AbiValue::Uint256(self.max_fee_per_gas),
152            AbiValue::Uint256(self.max_priority_fee_per_gas),
153            AbiValue::Bytes(self.paymaster_and_data.clone()),
154            AbiValue::Bytes(self.signature.clone()),
155        ])
156    }
157}
158
159// ═══════════════════════════════════════════════════════════════════
160// PackedUserOperation (v0.7)
161// ═══════════════════════════════════════════════════════════════════
162
163/// An ERC-4337 v0.7 PackedUserOperation.
164///
165/// In v0.7, gas fields are packed into 32-byte combined values:
166/// - `accountGasLimits` = `verificationGasLimit (16 bytes) || callGasLimit (16 bytes)`
167/// - `gasFees` = `maxPriorityFeePerGas (16 bytes) || maxFeePerGas (16 bytes)`
168#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct PackedUserOperation {
170    /// Smart account address.
171    pub sender: [u8; 20],
172    /// Anti-replay nonce.
173    pub nonce: [u8; 32],
174    /// Factory + factoryData for account creation (empty if exists).
175    pub init_code: Vec<u8>,
176    /// Encoded function call on the smart account.
177    pub call_data: Vec<u8>,
178    /// `verificationGasLimit (16) || callGasLimit (16)`
179    pub account_gas_limits: [u8; 32],
180    /// Pre-verification gas (`uint256`).
181    pub pre_verification_gas: Uint256,
182    /// `maxPriorityFeePerGas (16) || maxFeePerGas (16)`
183    pub gas_fees: [u8; 32],
184    /// Paymaster + data.
185    pub paymaster_and_data: Vec<u8>,
186    /// Signature.
187    pub signature: Vec<u8>,
188}
189
190/// Pack two u128 gas values into a 32-byte field.
191///
192/// `result = high (16 bytes BE) || low (16 bytes BE)`
193#[must_use]
194pub fn pack_gas(high: u128, low: u128) -> [u8; 32] {
195    let mut result = [0u8; 32];
196    result[..16].copy_from_slice(&high.to_be_bytes());
197    result[16..].copy_from_slice(&low.to_be_bytes());
198    result
199}
200
201/// Pack `verificationGasLimit` and `callGasLimit` into `accountGasLimits`.
202#[must_use]
203pub fn pack_account_gas_limits(verification_gas: u128, call_gas: u128) -> [u8; 32] {
204    pack_gas(verification_gas, call_gas)
205}
206
207/// Pack `maxPriorityFeePerGas` and `maxFeePerGas` into `gasFees`.
208#[must_use]
209pub fn pack_gas_fees(max_priority_fee: u128, max_fee: u128) -> [u8; 32] {
210    pack_gas(max_priority_fee, max_fee)
211}
212
213// ═══════════════════════════════════════════════════════════════════
214// CallData Builders
215// ═══════════════════════════════════════════════════════════════════
216
217/// Encode a single `execute(address dest, uint256 value, bytes calldata func)`.
218///
219/// Standard SimpleAccount execute function.
220#[must_use]
221pub fn encode_execute(dest: &[u8; 20], value: Uint256, func: &[u8]) -> Vec<u8> {
222    let execute = abi::Function::new("execute(address,uint256,bytes)");
223    execute.encode(&[
224        AbiValue::Address(*dest),
225        AbiValue::Uint256(value),
226        AbiValue::Bytes(func.to_vec()),
227    ])
228}
229
230/// Encode `executeBatch(address[] dest, uint256[] values, bytes[] func)`.
231///
232/// Batch execution for multiple calls in a single UserOp.
233#[must_use]
234pub fn encode_execute_batch(targets: &[[u8; 20]], values: &[Uint256], data: &[Vec<u8>]) -> Vec<u8> {
235    let batch = abi::Function::new("executeBatch(address[],uint256[],bytes[])");
236
237    let targets_abi: Vec<AbiValue> = targets.iter().map(|t| AbiValue::Address(*t)).collect();
238    let values_abi: Vec<AbiValue> = values.iter().map(|v| AbiValue::Uint256(*v)).collect();
239    let data_abi: Vec<AbiValue> = data.iter().map(|d| AbiValue::Bytes(d.clone())).collect();
240
241    batch.encode(&[
242        AbiValue::Array(targets_abi),
243        AbiValue::Array(values_abi),
244        AbiValue::Array(data_abi),
245    ])
246}
247
248/// Encode an ERC-20 `approve(address spender, uint256 amount)` call.
249#[must_use]
250pub fn encode_erc20_approve(spender: &[u8; 20], amount: Uint256) -> Vec<u8> {
251    let approve = abi::Function::new("approve(address,uint256)");
252    approve.encode(&[AbiValue::Address(*spender), AbiValue::Uint256(amount)])
253}
254
255/// Encode an ERC-20 `transfer(address to, uint256 amount)` call.
256#[must_use]
257pub fn encode_erc20_transfer(to: &[u8; 20], amount: Uint256) -> Vec<u8> {
258    let transfer = abi::Function::new("transfer(address,uint256)");
259    transfer.encode(&[AbiValue::Address(*to), AbiValue::Uint256(amount)])
260}
261
262/// Convert a u64 value into canonical uint256 encoding.
263#[must_use]
264pub fn uint256_from_u64(value: u64) -> Uint256 {
265    let mut out = [0u8; 32];
266    out[24..].copy_from_slice(&value.to_be_bytes());
267    out
268}
269
270// ═══════════════════════════════════════════════════════════════════
271// Tests
272// ═══════════════════════════════════════════════════════════════════
273
274#[cfg(test)]
275#[allow(clippy::unwrap_used, clippy::expect_used)]
276mod tests {
277    use super::*;
278
279    const SENDER: [u8; 20] = [0xAA; 20];
280    const DEST: [u8; 20] = [0xBB; 20];
281
282    #[test]
283    fn test_new_user_op_defaults() {
284        let op = UserOperation::new(SENDER);
285        assert_eq!(op.sender, SENDER);
286        assert_eq!(op.nonce, uint256_from_u64(0));
287        assert!(op.init_code.is_empty());
288        assert!(op.call_data.is_empty());
289        assert!(op.paymaster_and_data.is_empty());
290        assert!(op.signature.is_empty());
291    }
292
293    #[test]
294    fn test_user_op_hash_deterministic() {
295        let op = UserOperation::new(SENDER);
296        let h1 = op.hash(&ENTRY_POINT_V06, uint256_from_u64(1));
297        let h2 = op.hash(&ENTRY_POINT_V06, uint256_from_u64(1));
298        assert_eq!(h1, h2);
299    }
300
301    #[test]
302    fn test_user_op_hash_different_chain_ids() {
303        let op = UserOperation::new(SENDER);
304        let h1 = op.hash(&ENTRY_POINT_V06, uint256_from_u64(1));
305        let h2 = op.hash(&ENTRY_POINT_V06, uint256_from_u64(137));
306        assert_ne!(h1, h2, "different chains should produce different hashes");
307    }
308
309    #[test]
310    fn test_user_op_hash_different_nonces() {
311        let mut op1 = UserOperation::new(SENDER);
312        let mut op2 = UserOperation::new(SENDER);
313        op1.nonce = uint256_from_u64(0);
314        op2.nonce = uint256_from_u64(1);
315        assert_ne!(
316            op1.hash(&ENTRY_POINT_V06, uint256_from_u64(1)),
317            op2.hash(&ENTRY_POINT_V06, uint256_from_u64(1)),
318        );
319    }
320
321    #[test]
322    fn test_user_op_encode_changes_with_data() {
323        let op1 = UserOperation::new(SENDER);
324        let mut op2 = UserOperation::new(SENDER);
325        op2.call_data = vec![0xDE, 0xAD];
326        assert_ne!(op1.encode(), op2.encode());
327    }
328
329    #[test]
330    fn test_encode_execute_selector() {
331        let data = encode_execute(&DEST, uint256_from_u64(0), &[]);
332        let expected = abi::function_selector("execute(address,uint256,bytes)");
333        assert_eq!(&data[..4], &expected);
334    }
335
336    #[test]
337    fn test_encode_execute_batch_selector() {
338        let data = encode_execute_batch(&[DEST], &[uint256_from_u64(0)], &[vec![]]);
339        let expected = abi::function_selector("executeBatch(address[],uint256[],bytes[])");
340        assert_eq!(&data[..4], &expected);
341    }
342
343    #[test]
344    fn test_encode_erc20_approve_selector() {
345        let data = encode_erc20_approve(&DEST, uint256_from_u64(1000));
346        let expected = abi::function_selector("approve(address,uint256)");
347        assert_eq!(&data[..4], &expected);
348    }
349
350    #[test]
351    fn test_encode_erc20_transfer_selector() {
352        let data = encode_erc20_transfer(&DEST, uint256_from_u64(500));
353        let expected = abi::function_selector("transfer(address,uint256)");
354        assert_eq!(&data[..4], &expected);
355    }
356
357    #[test]
358    fn test_pack_gas_basic() {
359        let packed = pack_gas(100, 200);
360        let high = u128::from_be_bytes(packed[..16].try_into().unwrap());
361        let low = u128::from_be_bytes(packed[16..].try_into().unwrap());
362        assert_eq!(high, 100);
363        assert_eq!(low, 200);
364    }
365
366    #[test]
367    fn test_entry_point_addresses_different() {
368        assert_ne!(ENTRY_POINT_V06, ENTRY_POINT_V07);
369    }
370
371    #[test]
372    fn test_e2e_user_op_with_execute() {
373        let mut op = UserOperation::new(SENDER);
374        op.nonce = uint256_from_u64(1);
375        op.call_data = encode_execute(&DEST, uint256_from_u64(1_000_000), &[]);
376        op.call_gas_limit = uint256_from_u64(200_000);
377        let hash = op.hash(&ENTRY_POINT_V06, uint256_from_u64(1));
378        assert_ne!(hash, [0u8; 32]);
379    }
380
381    #[test]
382    fn test_e2e_user_op_with_batch() {
383        let mut op = UserOperation::new(SENDER);
384        op.call_data = encode_execute_batch(
385            &[DEST, [0xCC; 20]],
386            &[uint256_from_u64(100), uint256_from_u64(200)],
387            &[
388                encode_erc20_transfer(&[0xDD; 20], uint256_from_u64(500)),
389                vec![],
390            ],
391        );
392        let hash = op.hash(&ENTRY_POINT_V06, uint256_from_u64(1));
393        assert_ne!(hash, [0u8; 32]);
394    }
395}