1+ // SPDX-License-Identifier: MIT
2+ pragma solidity ^ 0.8.0 ;
3+
4+ import {console2} from "forge-std/Test.sol " ;
5+ import {UserOperation} from "account-abstraction/interfaces/UserOperation.sol " ;
6+
7+ import {CoinbaseSmartWallet} from "../../src/CoinbaseSmartWallet.sol " ;
8+ import {CoinbaseSmartWalletFactory} from "../../src/CoinbaseSmartWalletFactory.sol " ;
9+ import {MockERC20} from "../../lib/solady/test/utils/mocks/MockERC20.sol " ;
10+
11+ import {MockTarget} from "../mocks/MockTarget.sol " ;
12+ import {SmartWalletTestBase} from "../CoinbaseSmartWallet/SmartWalletTestBase.sol " ;
13+ import {Static} from "../CoinbaseSmartWallet/Static.sol " ;
14+
15+ /// @title EndToEndTest
16+ /// @notice Gas comparison tests between ERC-4337 Base Account and EOA transactions
17+ /// @dev Isolated test contract to measure gas consumption for common operations
18+ /// Tests ran using `FOUNDRY_PROFILE=deploy` to simulate real-world gas costs
19+ /// forge-config: default.isolate = true
20+ contract EndToEndTest is SmartWalletTestBase {
21+ address eoaUser = address (0xe0a );
22+ MockERC20 usdc;
23+ MockTarget target;
24+ CoinbaseSmartWalletFactory factory;
25+
26+ function setUp () public override {
27+ // Deploy EntryPoint at canonical address
28+ vm.etch (0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789 , Static.ENTRY_POINT_BYTES);
29+
30+ // Deploy smart wallet infrastructure
31+ CoinbaseSmartWallet implementation = new CoinbaseSmartWallet ();
32+ factory = new CoinbaseSmartWalletFactory (address (implementation));
33+
34+ // Configure wallet owner
35+ signerPrivateKey = 0xa11ce ;
36+ signer = vm.addr (signerPrivateKey);
37+ owners.push (abi.encode (signer));
38+ account = factory.createAccount (owners, 0 );
39+
40+ // Fund wallets with ETH
41+ vm.deal (address (account), 100 ether);
42+ vm.deal (eoaUser, 100 ether);
43+
44+ // Deploy and mint USDC tokens
45+ usdc = new MockERC20 ("USD Coin " , "USDC " , 6 );
46+ usdc.mint (address (account), 10000e6 );
47+ usdc.mint (eoaUser, 10000e6 );
48+
49+ target = new MockTarget ();
50+ }
51+
52+ // Native ETH Transfer - Base Account
53+ function test_transfer_native_baseAccount () public {
54+ // Dust recipient to avoid gas changes for first non-zero balance
55+ vm.deal (address (0x1234 ), 1 wei);
56+
57+ // Prepare UserOperation for native ETH transfer
58+ userOpCalldata = abi.encodeCall (CoinbaseSmartWallet.execute, (address (0x1234 ), 1 ether, "" ));
59+ UserOperation memory op = _getUserOpWithSignature ();
60+
61+ // Measure calldata size
62+ bytes memory handleOpsCalldata = abi.encodeCall (entryPoint.handleOps, (_makeOpsArray (op), payable (bundler)));
63+ console2.log ("test_transfer_native Base Account calldata size: " , handleOpsCalldata.length );
64+
65+ // Execute and measure gas
66+ vm.startSnapshotGas ("e2e_transfer_native_baseAccount " );
67+ _sendUserOperation (op);
68+ uint256 gasUsed = vm.stopSnapshotGas ();
69+ console2.log ("test_transfer_native Base Account gas: " , gasUsed);
70+ }
71+
72+ // Native ETH Transfer - EOA
73+ function test_transfer_native_eoa () public {
74+ // Dust recipient to avoid gas changes for first non-zero balance
75+ vm.deal (address (0x1234 ), 1 wei);
76+
77+ console2.log ("test_transfer_native EOA calldata size: " , uint256 (0 ));
78+
79+ // Execute and measure gas
80+ vm.prank (eoaUser);
81+ vm.startSnapshotGas ("e2e_transfer_native_eoa " );
82+ payable (address (0x1234 )).transfer (1 ether);
83+ uint256 gasUsed = vm.stopSnapshotGas ();
84+ console2.log ("test_transfer_native EOA gas: " , gasUsed);
85+ }
86+
87+ // ERC20 Transfer - Base Account
88+ function test_transfer_erc20_baseAccount () public {
89+ // Dust recipient to avoid gas changes for first non-zero balance
90+ vm.deal (address (0x5678 ), 1 wei);
91+ usdc.mint (address (0x5678 ), 1 wei);
92+
93+ // Prepare UserOperation for ERC20 transfer
94+ userOpCalldata = abi.encodeCall (
95+ CoinbaseSmartWallet.execute, (address (usdc), 0 , abi.encodeCall (usdc.transfer, (address (0x5678 ), 100e6 )))
96+ );
97+ UserOperation memory op = _getUserOpWithSignature ();
98+
99+ // Measure calldata size
100+ bytes memory handleOpsCalldata = abi.encodeCall (entryPoint.handleOps, (_makeOpsArray (op), payable (bundler)));
101+ console2.log ("test_transfer_erc20 Base Account calldata size: " , handleOpsCalldata.length );
102+
103+ // Execute and measure gas
104+ vm.startSnapshotGas ("e2e_transfer_erc20_baseAccount " );
105+ _sendUserOperation (op);
106+ uint256 gasUsed = vm.stopSnapshotGas ();
107+ console2.log ("test_transfer_erc20 Base Account gas: " , gasUsed);
108+ }
109+
110+ // ERC20 Transfer - EOA
111+ function test_transfer_erc20_eoa () public {
112+ // Dust recipient to avoid gas changes for first non-zero balance
113+ vm.deal (address (0x5678 ), 1 wei);
114+ usdc.mint (address (0x5678 ), 1 wei);
115+
116+ // Measure calldata size
117+ bytes memory eoaCalldata = abi.encodeCall (usdc.transfer, (address (0x5678 ), 100e6 ));
118+ console2.log ("test_transfer_erc20 EOA calldata size: " , eoaCalldata.length );
119+
120+ // Execute and measure gas
121+ vm.prank (eoaUser);
122+ vm.startSnapshotGas ("e2e_transfer_erc20_eoa " );
123+ usdc.transfer (address (0x5678 ), 100e6 );
124+ uint256 gasUsed = vm.stopSnapshotGas ();
125+ console2.log ("test_transfer_erc20 EOA gas: " , gasUsed);
126+ }
127+
128+ // Helper Functions
129+ // Creates an array containing a single UserOperation
130+ function _makeOpsArray (UserOperation memory op ) internal pure returns (UserOperation[] memory ) {
131+ UserOperation[] memory ops = new UserOperation [](1 );
132+ ops[0 ] = op;
133+ return ops;
134+ }
135+
136+ // Signs a UserOperation with the configured signer
137+ // Overrides the parent implementation to use the configured signer
138+ function _sign (UserOperation memory userOp ) internal view override returns (bytes memory signature ) {
139+ bytes32 toSign = entryPoint.getUserOpHash (userOp);
140+ (uint8 v , bytes32 r , bytes32 s ) = vm.sign (signerPrivateKey, toSign);
141+ signature = abi.encode (CoinbaseSmartWallet.SignatureWrapper (0 , abi.encodePacked (r, s, v)));
142+ }
143+ }
0 commit comments